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/). - -![txt2img-stable2](assets/stable-samples/txt2img/merged-0006.png) -[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: -![sd evaluation results](assets/v1-variants-scores.jpg) - - - -### Text-to-Image with Stable Diffusion -![txt2img-stable2](assets/stable-samples/txt2img/merged-0005.png) -![txt2img-stable2](assets/stable-samples/txt2img/merged-0007.png) - -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** - -![sketch-in](assets/stable-samples/img2img/sketch-mountains-input.jpg) - -**Outputs** - -![out3](assets/stable-samples/img2img/mountains-3.png) -![out2](assets/stable-samples/img2img/mountains-2.png) - -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

- -

- -

- -

- last-commit - stars -
- issues - pull-requests -

- -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. +![project hero](https://github.com/invoke-ai/InvokeAI/assets/31807370/6e3728c7-e90e-4711-905c-3b55844ff5be) -``` -(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 In Colab
-Open and follow instructions to use an isolated environment running Dream.
- -Output example: -![Colab Notebook](static/colab_notebook.png) - -## 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. - -![Dream Web Server](static/dream_web_server.png) - -## 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. +![Highlighted Features - Canvas and Workflows](https://github.com/invoke-ai/InvokeAI/assets/31807370/708f7a82-084f-4860-bfbe-e2588c53548d) --- - -- 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", - "\"stable-diffusion-ai\"
\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: -![pareto](assets/v1-variants-scores.jpg) +![pareto](assets/v1-variants-scores.jpg) 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` + +![Anime Comparison Grid](https://user-images.githubusercontent.com/50542132/191868725-7f7af991-e254-4c1f-83e7-bed8c9b2d34f.png) + +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` + +![Nature Comparison Grid](https://user-images.githubusercontent.com/50542132/191868763-b151c69e-0a72-4cf1-a151-5a64edd0c93e.png) + +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` + +![Food Comparison Grid](https://user-images.githubusercontent.com/50542132/191868898-98801a62-885f-4ea1-aee8-563503522aa9.png) + +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` + +![Animal Comparison Grid](https://user-images.githubusercontent.com/50542132/191868870-9e3b7d82-b909-429f-893a-13f6ec343454.png) + +`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).* + +![People Comparison Grid 1](https://user-images.githubusercontent.com/50542132/191871743-6802f199-0ffd-4986-98c5-df2d8db30d18.png) + +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. + +![People Comparison Grid 2](https://user-images.githubusercontent.com/50542132/191992123-7e0759d6-6220-42c4-a961-88c7071c5ee6.png) + +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`. +::: + +![Low Step Sampler Comparison](https://user-images.githubusercontent.com/50542132/192046823-2714cb29-bbf3-4eb1-9213-e27a0963905c.png) 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**. + +![cuda-sysmem-fallback](./assets/cuda-sysmem-fallback.png) + +:::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. + +![resize invocation](./assets/resize_invocation.png) + +When you launch the frontend UI, you can go to the Node Editor tab and find your +new Invocation ready to be used. + +![resize node editor](./assets/resize_node_editor.png) + +## 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" +![Alt text here](./assets/demonstration.webp) +``` + +### 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** + +![InvokeAI](https://invoke-ai.github.io/InvokeAI/assets/invoke_ai_banner.png) +``` 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 + ![html-overview](./assets/html-overview.png) + + ![html-detail](./assets/html-detail.png) +::: 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: + +![Administrator Setup Screen](./assets/admin-setup.png) + +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" + +![Administrator Menu](./assets/admin-add-user-1.png) + +This will take you to the User Management screen... + +![User Management screen](./assets/admin-add-user-2.png) + +...where you can click "Create User" to add a new user. + +![Add User Screen](./assets/admin-add-user-3.png) + +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: + +![Login Screen](./assets/user-login-1.png) + +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** + +
+ Before background removal + After background removal +
+ +--- + +### 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:** + +
+ Save Image as AVIF + Save Image as JPEG + Save Image as PNG + Save Image as TIFF + Save Image as WebP +
+ +--- + +### 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:** + +![ollama node](https://raw.githubusercontent.com/Jonseed/Ollama-Node/a3e7cdc55e394cb89c1ea7ed54e106c212c85e8c/ollama-node-screenshot.png) + +--- + +### 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. + + ![Workflow Library](./assets/workflow_library.png) + + 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! + + + ![Linear View](./assets/linearview.png) + + + 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. + + ![Create Latent Noise](./assets/groupsnoise.png) + + ### 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. + + ![Text Prompt Conditioning](./assets/groupsconditioning.png) + + + + ### 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. + + ![Image to Latents & VAE](./assets/groupsimgvae.png) + + ### 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. + + ![Scaling Nodes](./assets/groupsallscale.png) + + + + ### 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. + + ![ControlNet Setup](./assets/groupscontrol.png) + + ### 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. + + ![LoRA Setup](./assets/groupslora.png) + + + + ### 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. + + ![Defined & Random Seeds](./assets/groupsnoise.png) + + ### 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. + + ![Iteration](./assets/groupsiterate.png) + + ### 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. + + ![Batch Generation](./assets/groupsmultigenseeding.png) + + 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. + +![Gallery Panel Overview](./assets/gallery.png) + +--- + +## Board Display and Settings + +At the very top of the Gallery Panel, you will find the board disclosure and settings buttons. + +![Top Controls](./assets/top_controls.png) + +The **disclosure button** shows the name of the currently selected board and allows you to toggle the visibility of the board thumbnails. + +![Board Thumbnails](./assets/board_thumbnails.png) + +The **settings button** opens a list of customization options: + +![Board Settings](./assets/board_settings.png) + +- **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). + +![Thumbnail Menu](./assets/thumbnail_menu.png) + +- **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: + +![Board Tabs](./assets/board_tabs.png) + +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** ![Info Button](./assets/info_button.png) in any result panel. + +Additionally, each image has a context menu (right-click or Ctrl+click) with powerful workflow actions: + +![Image Menu](./assets/image_menu.png) + +*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'; + +
+ Invoke WebUI +
+ +## 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'; +--- + + + +
+ This site was designed and developed by Aether Fox Studio. +
+ + 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 ── */} + + + + + + + + + {iface.methods.map((method) => ( + + + + ))} + +
MethodDescription
{method.name} +
+ + {/* ── Per-method details ── */} + {iface.methods.map((method) => ( +
+

{method.name}

+ +
{method.signature}
+ + {method.description && ( +
+ )} + + {method.parameters.length > 0 && ( + <> + + + + + + + + + + + + {method.parameters.map((param) => ( + + + + + + ))} + +
NameTypeDescriptionDefault
{param.name}{param.type || '—'} + {param.default ? {param.default} : required}
+ + )} + + {(method.returns || method.return_type) && ( + <> + + + + + + + + + + + + + +
TypeDescription
{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, + }, + ], +]; +--- + + + + 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 + ], + [ + "", + 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 + ], + [ + "", + 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 + ], + [ + "", + 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 + ] + ], + "byte_fallback": false + } +} \ No newline at end of file diff --git a/invokeai/backend/anima/tokenizer/tokenizer_config.json b/invokeai/backend/anima/tokenizer/tokenizer_config.json new file mode 100644 index 00000000000..90c0450f186 --- /dev/null +++ b/invokeai/backend/anima/tokenizer/tokenizer_config.json @@ -0,0 +1,941 @@ +{ + "add_prefix_space": null, + "added_tokens_decoder": { + "0": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "1": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "2": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32000": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32001": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32002": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32003": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32004": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32005": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32006": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32007": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32008": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32009": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32010": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32011": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32012": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32013": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32014": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32015": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32016": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32017": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32018": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32019": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32020": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32021": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32022": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32023": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32024": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32025": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32026": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32027": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32028": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32029": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32030": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32031": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32032": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32033": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32034": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32035": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32036": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32037": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32038": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32039": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32040": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32041": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32042": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32043": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32044": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32045": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32046": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32047": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32048": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32049": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32050": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32051": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32052": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32053": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32054": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32055": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32056": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32057": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32058": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32059": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32060": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32061": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32062": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32063": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32064": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32065": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32066": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32067": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32068": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32069": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32070": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32071": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32072": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32073": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32074": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32075": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32076": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32077": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32078": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32079": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32080": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32081": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32082": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32083": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32084": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32085": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32086": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32087": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32088": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32089": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32090": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32091": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32092": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32093": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32094": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32095": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32096": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32097": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32098": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + }, + "32099": { + "content": "", + "lstrip": false, + "normalized": false, + "rstrip": false, + "single_word": false, + "special": true + } + }, + "additional_special_tokens": [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ], + "clean_up_tokenization_spaces": false, + "eos_token": "", + "extra_ids": 100, + "extra_special_tokens": {}, + "legacy": true, + "model_max_length": 512, + "pad_token": "", + "sp_model_kwargs": {}, + "tokenizer_class": "T5Tokenizer", + "unk_token": "" +} diff --git a/invokeai/backend/flux/controlnet/__init__.py b/invokeai/backend/flux/controlnet/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/flux/controlnet/controlnet_flux_output.py b/invokeai/backend/flux/controlnet/controlnet_flux_output.py new file mode 100644 index 00000000000..55940460c34 --- /dev/null +++ b/invokeai/backend/flux/controlnet/controlnet_flux_output.py @@ -0,0 +1,58 @@ +from dataclasses import dataclass + +import torch + + +@dataclass +class ControlNetFluxOutput: + single_block_residuals: list[torch.Tensor] | None + double_block_residuals: list[torch.Tensor] | None + + def apply_weight(self, weight: float): + if self.single_block_residuals is not None: + for i in range(len(self.single_block_residuals)): + self.single_block_residuals[i] = self.single_block_residuals[i] * weight + if self.double_block_residuals is not None: + for i in range(len(self.double_block_residuals)): + self.double_block_residuals[i] = self.double_block_residuals[i] * weight + + +def add_tensor_lists_elementwise( + list1: list[torch.Tensor] | None, list2: list[torch.Tensor] | None +) -> list[torch.Tensor] | None: + """Add two tensor lists elementwise that could be None.""" + if list1 is None and list2 is None: + return None + if list1 is None: + return list2 + if list2 is None: + return list1 + + new_list: list[torch.Tensor] = [] + for list1_tensor, list2_tensor in zip(list1, list2, strict=True): + new_list.append(list1_tensor + list2_tensor) + return new_list + + +def add_controlnet_flux_outputs( + controlnet_output_1: ControlNetFluxOutput, controlnet_output_2: ControlNetFluxOutput +) -> ControlNetFluxOutput: + return ControlNetFluxOutput( + single_block_residuals=add_tensor_lists_elementwise( + controlnet_output_1.single_block_residuals, controlnet_output_2.single_block_residuals + ), + double_block_residuals=add_tensor_lists_elementwise( + controlnet_output_1.double_block_residuals, controlnet_output_2.double_block_residuals + ), + ) + + +def sum_controlnet_flux_outputs( + controlnet_outputs: list[ControlNetFluxOutput], +) -> ControlNetFluxOutput: + controlnet_output_sum = ControlNetFluxOutput(single_block_residuals=None, double_block_residuals=None) + + for controlnet_output in controlnet_outputs: + controlnet_output_sum = add_controlnet_flux_outputs(controlnet_output_sum, controlnet_output) + + return controlnet_output_sum diff --git a/invokeai/backend/flux/controlnet/instantx_controlnet_flux.py b/invokeai/backend/flux/controlnet/instantx_controlnet_flux.py new file mode 100644 index 00000000000..1af5fbdfc09 --- /dev/null +++ b/invokeai/backend/flux/controlnet/instantx_controlnet_flux.py @@ -0,0 +1,180 @@ +# This file was initially copied from: +# https://github.com/huggingface/diffusers/blob/99f608218caa069a2f16dcf9efab46959b15aec0/src/diffusers/models/controlnet_flux.py + + +from dataclasses import dataclass + +import torch +import torch.nn as nn + +from invokeai.backend.flux.controlnet.zero_module import zero_module +from invokeai.backend.flux.model import FluxParams +from invokeai.backend.flux.modules.layers import ( + DoubleStreamBlock, + EmbedND, + MLPEmbedder, + SingleStreamBlock, + timestep_embedding, +) + + +@dataclass +class InstantXControlNetFluxOutput: + controlnet_block_samples: list[torch.Tensor] | None + controlnet_single_block_samples: list[torch.Tensor] | None + + +# NOTE(ryand): Mapping between diffusers FLUX transformer params and BFL FLUX transformer params: +# - Diffusers: BFL +# - in_channels: in_channels +# - num_layers: depth +# - num_single_layers: depth_single_blocks +# - attention_head_dim: hidden_size // num_heads +# - num_attention_heads: num_heads +# - joint_attention_dim: context_in_dim +# - pooled_projection_dim: vec_in_dim +# - guidance_embeds: guidance_embed +# - axes_dims_rope: axes_dim + + +class InstantXControlNetFlux(torch.nn.Module): + def __init__(self, params: FluxParams, num_control_modes: int | None = None): + """ + Args: + params (FluxParams): The parameters for the FLUX model. + num_control_modes (int | None, optional): The number of controlnet modes. If non-None, then the model is a + 'union controlnet' model and expects a mode conditioning input at runtime. + """ + super().__init__() + + # The following modules mirror the base FLUX transformer model. + # ------------------------------------------------------------- + self.params = params + self.in_channels = params.in_channels + self.out_channels = self.in_channels + if params.hidden_size % params.num_heads != 0: + raise ValueError(f"Hidden size {params.hidden_size} must be divisible by num_heads {params.num_heads}") + pe_dim = params.hidden_size // params.num_heads + if sum(params.axes_dim) != pe_dim: + raise ValueError(f"Got {params.axes_dim} but expected positional dim {pe_dim}") + self.hidden_size = params.hidden_size + self.num_heads = params.num_heads + self.pe_embedder = EmbedND(dim=pe_dim, theta=params.theta, axes_dim=params.axes_dim) + self.img_in = nn.Linear(self.in_channels, self.hidden_size, bias=True) + self.time_in = MLPEmbedder(in_dim=256, hidden_dim=self.hidden_size) + self.vector_in = MLPEmbedder(params.vec_in_dim, self.hidden_size) + self.guidance_in = ( + MLPEmbedder(in_dim=256, hidden_dim=self.hidden_size) if params.guidance_embed else nn.Identity() + ) + self.txt_in = nn.Linear(params.context_in_dim, self.hidden_size) + + self.double_blocks = nn.ModuleList( + [ + DoubleStreamBlock( + self.hidden_size, + self.num_heads, + mlp_ratio=params.mlp_ratio, + qkv_bias=params.qkv_bias, + ) + for _ in range(params.depth) + ] + ) + + self.single_blocks = nn.ModuleList( + [ + SingleStreamBlock(self.hidden_size, self.num_heads, mlp_ratio=params.mlp_ratio) + for _ in range(params.depth_single_blocks) + ] + ) + + # The following modules are specific to the ControlNet model. + # ----------------------------------------------------------- + self.controlnet_blocks = nn.ModuleList([]) + for _ in range(len(self.double_blocks)): + self.controlnet_blocks.append(zero_module(nn.Linear(self.hidden_size, self.hidden_size))) + + self.controlnet_single_blocks = nn.ModuleList([]) + for _ in range(len(self.single_blocks)): + self.controlnet_single_blocks.append(zero_module(nn.Linear(self.hidden_size, self.hidden_size))) + + self.is_union = False + if num_control_modes is not None: + self.is_union = True + self.controlnet_mode_embedder = nn.Embedding(num_control_modes, self.hidden_size) + + self.controlnet_x_embedder = zero_module(torch.nn.Linear(self.in_channels, self.hidden_size)) + + def forward( + self, + controlnet_cond: torch.Tensor, + controlnet_mode: torch.Tensor | None, + img: torch.Tensor, + img_ids: torch.Tensor, + txt: torch.Tensor, + txt_ids: torch.Tensor, + timesteps: torch.Tensor, + y: torch.Tensor, + guidance: torch.Tensor | None = None, + ) -> InstantXControlNetFluxOutput: + if img.ndim != 3 or txt.ndim != 3: + raise ValueError("Input img and txt tensors must have 3 dimensions.") + + img = self.img_in(img) + + # Add controlnet_cond embedding. + img = img + self.controlnet_x_embedder(controlnet_cond) + + vec = self.time_in(timestep_embedding(timesteps, 256)) + if self.params.guidance_embed: + if guidance is None: + raise ValueError("Didn't get guidance strength for guidance distilled model.") + vec = vec + self.guidance_in(timestep_embedding(guidance, 256)) + vec = vec + self.vector_in(y) + txt = self.txt_in(txt) + + # If this is a union ControlNet, then concat the control mode embedding to the T5 text embedding. + if self.is_union: + if controlnet_mode is None: + # We allow users to enter 'None' as the controlnet_mode if they don't want to worry about this input. + # We've chosen to use a zero-embedding in this case. + zero_index = torch.zeros([1, 1], dtype=torch.long, device=txt.device) + controlnet_mode_emb = torch.zeros_like(self.controlnet_mode_embedder(zero_index)) + else: + controlnet_mode_emb = self.controlnet_mode_embedder(controlnet_mode) + txt = torch.cat([controlnet_mode_emb, txt], dim=1) + txt_ids = torch.cat([txt_ids[:, :1, :], txt_ids], dim=1) + else: + assert controlnet_mode is None + + ids = torch.cat((txt_ids, img_ids), dim=1) + pe = self.pe_embedder(ids) + + double_block_samples: list[torch.Tensor] = [] + for block in self.double_blocks: + img, txt = block(img=img, txt=txt, vec=vec, pe=pe) + double_block_samples.append(img) + + img = torch.cat((txt, img), 1) + + single_block_samples: list[torch.Tensor] = [] + for block in self.single_blocks: + img = block(img, vec=vec, pe=pe) + single_block_samples.append(img[:, txt.shape[1] :]) + + # ControlNet Block + controlnet_double_block_samples: list[torch.Tensor] = [] + for double_block_sample, controlnet_block in zip(double_block_samples, self.controlnet_blocks, strict=True): + double_block_sample = controlnet_block(double_block_sample) + controlnet_double_block_samples.append(double_block_sample) + + controlnet_single_block_samples: list[torch.Tensor] = [] + for single_block_sample, controlnet_block in zip( + single_block_samples, self.controlnet_single_blocks, strict=True + ): + single_block_sample = controlnet_block(single_block_sample) + controlnet_single_block_samples.append(single_block_sample) + + return InstantXControlNetFluxOutput( + controlnet_block_samples=controlnet_double_block_samples or None, + controlnet_single_block_samples=controlnet_single_block_samples or None, + ) diff --git a/invokeai/backend/flux/controlnet/state_dict_utils.py b/invokeai/backend/flux/controlnet/state_dict_utils.py new file mode 100644 index 00000000000..87eae5a96bc --- /dev/null +++ b/invokeai/backend/flux/controlnet/state_dict_utils.py @@ -0,0 +1,295 @@ +from typing import Any, Dict + +import torch + +from invokeai.backend.flux.model import FluxParams + + +def is_state_dict_xlabs_controlnet(sd: dict[str | int, Any]) -> bool: + """Is the state dict for an XLabs ControlNet model? + + This is intended to be a reasonably high-precision detector, but it is not guaranteed to have perfect precision. + """ + # If all of the expected keys are present, then this is very likely an XLabs ControlNet model. + expected_keys = { + "controlnet_blocks.0.bias", + "controlnet_blocks.0.weight", + "input_hint_block.0.bias", + "input_hint_block.0.weight", + "pos_embed_input.bias", + "pos_embed_input.weight", + } + + if expected_keys.issubset(sd.keys()): + return True + return False + + +def is_state_dict_instantx_controlnet(sd: dict[str | int, Any]) -> bool: + """Is the state dict for an InstantX ControlNet model? + + This is intended to be a reasonably high-precision detector, but it is not guaranteed to have perfect precision. + """ + # If all of the expected keys are present, then this is very likely an InstantX ControlNet model. + expected_keys = { + "controlnet_blocks.0.bias", + "controlnet_blocks.0.weight", + "controlnet_x_embedder.bias", + "controlnet_x_embedder.weight", + } + + if expected_keys.issubset(sd.keys()): + return True + return False + + +def _fuse_weights(*t: torch.Tensor) -> torch.Tensor: + """Fuse weights along dimension 0. + + Used to fuse q, k, v attention weights into a single qkv tensor when converting from diffusers to BFL format. + """ + # TODO(ryand): Double check dim=0 is correct. + return torch.cat(t, dim=0) + + +def _convert_flux_double_block_sd_from_diffusers_to_bfl_format( + sd: Dict[str, torch.Tensor], double_block_index: int +) -> Dict[str, torch.Tensor]: + """Convert the state dict for a double block from diffusers format to BFL format.""" + to_prefix = f"double_blocks.{double_block_index}" + from_prefix = f"transformer_blocks.{double_block_index}" + + new_sd: dict[str, torch.Tensor] = {} + + # Check one key to determine if this block exists. + if f"{from_prefix}.attn.add_q_proj.bias" not in sd: + return new_sd + + # txt_attn.qkv + new_sd[f"{to_prefix}.txt_attn.qkv.bias"] = _fuse_weights( + sd.pop(f"{from_prefix}.attn.add_q_proj.bias"), + sd.pop(f"{from_prefix}.attn.add_k_proj.bias"), + sd.pop(f"{from_prefix}.attn.add_v_proj.bias"), + ) + new_sd[f"{to_prefix}.txt_attn.qkv.weight"] = _fuse_weights( + sd.pop(f"{from_prefix}.attn.add_q_proj.weight"), + sd.pop(f"{from_prefix}.attn.add_k_proj.weight"), + sd.pop(f"{from_prefix}.attn.add_v_proj.weight"), + ) + + # img_attn.qkv + new_sd[f"{to_prefix}.img_attn.qkv.bias"] = _fuse_weights( + sd.pop(f"{from_prefix}.attn.to_q.bias"), + sd.pop(f"{from_prefix}.attn.to_k.bias"), + sd.pop(f"{from_prefix}.attn.to_v.bias"), + ) + new_sd[f"{to_prefix}.img_attn.qkv.weight"] = _fuse_weights( + sd.pop(f"{from_prefix}.attn.to_q.weight"), + sd.pop(f"{from_prefix}.attn.to_k.weight"), + sd.pop(f"{from_prefix}.attn.to_v.weight"), + ) + + # Handle basic 1-to-1 key conversions. + key_map = { + # img_attn + "attn.norm_k.weight": "img_attn.norm.key_norm.scale", + "attn.norm_q.weight": "img_attn.norm.query_norm.scale", + "attn.to_out.0.weight": "img_attn.proj.weight", + "attn.to_out.0.bias": "img_attn.proj.bias", + # img_mlp + "ff.net.0.proj.weight": "img_mlp.0.weight", + "ff.net.0.proj.bias": "img_mlp.0.bias", + "ff.net.2.weight": "img_mlp.2.weight", + "ff.net.2.bias": "img_mlp.2.bias", + # img_mod + "norm1.linear.weight": "img_mod.lin.weight", + "norm1.linear.bias": "img_mod.lin.bias", + # txt_attn + "attn.norm_added_q.weight": "txt_attn.norm.query_norm.scale", + "attn.norm_added_k.weight": "txt_attn.norm.key_norm.scale", + "attn.to_add_out.weight": "txt_attn.proj.weight", + "attn.to_add_out.bias": "txt_attn.proj.bias", + # txt_mlp + "ff_context.net.0.proj.weight": "txt_mlp.0.weight", + "ff_context.net.0.proj.bias": "txt_mlp.0.bias", + "ff_context.net.2.weight": "txt_mlp.2.weight", + "ff_context.net.2.bias": "txt_mlp.2.bias", + # txt_mod + "norm1_context.linear.weight": "txt_mod.lin.weight", + "norm1_context.linear.bias": "txt_mod.lin.bias", + } + for from_key, to_key in key_map.items(): + new_sd[f"{to_prefix}.{to_key}"] = sd.pop(f"{from_prefix}.{from_key}") + + return new_sd + + +def _convert_flux_single_block_sd_from_diffusers_to_bfl_format( + sd: Dict[str, torch.Tensor], single_block_index: int +) -> Dict[str, torch.Tensor]: + """Convert the state dict for a single block from diffusers format to BFL format.""" + to_prefix = f"single_blocks.{single_block_index}" + from_prefix = f"single_transformer_blocks.{single_block_index}" + + new_sd: dict[str, torch.Tensor] = {} + + # Check one key to determine if this block exists. + if f"{from_prefix}.attn.to_q.bias" not in sd: + return new_sd + + # linear1 (qkv) + new_sd[f"{to_prefix}.linear1.bias"] = _fuse_weights( + sd.pop(f"{from_prefix}.attn.to_q.bias"), + sd.pop(f"{from_prefix}.attn.to_k.bias"), + sd.pop(f"{from_prefix}.attn.to_v.bias"), + sd.pop(f"{from_prefix}.proj_mlp.bias"), + ) + new_sd[f"{to_prefix}.linear1.weight"] = _fuse_weights( + sd.pop(f"{from_prefix}.attn.to_q.weight"), + sd.pop(f"{from_prefix}.attn.to_k.weight"), + sd.pop(f"{from_prefix}.attn.to_v.weight"), + sd.pop(f"{from_prefix}.proj_mlp.weight"), + ) + + # Handle basic 1-to-1 key conversions. + key_map = { + # linear2 + "proj_out.weight": "linear2.weight", + "proj_out.bias": "linear2.bias", + # modulation + "norm.linear.weight": "modulation.lin.weight", + "norm.linear.bias": "modulation.lin.bias", + # norm + "attn.norm_k.weight": "norm.key_norm.scale", + "attn.norm_q.weight": "norm.query_norm.scale", + } + for from_key, to_key in key_map.items(): + new_sd[f"{to_prefix}.{to_key}"] = sd.pop(f"{from_prefix}.{from_key}") + + return new_sd + + +def convert_diffusers_instantx_state_dict_to_bfl_format(sd: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]: + """Convert an InstantX ControlNet state dict to the format that can be loaded by our internal + InstantXControlNetFlux model. + + The original InstantX ControlNet model was developed to be used in diffusers. We have ported the original + implementation to InstantXControlNetFlux to make it compatible with BFL-style models. This function converts the + original state dict to the format expected by InstantXControlNetFlux. + """ + # Shallow copy sd so that we can pop keys from it without modifying the original. + sd = sd.copy() + + new_sd: dict[str, torch.Tensor] = {} + + # Handle basic 1-to-1 key conversions. + basic_key_map = { + # Base model keys. + # ---------------- + # txt_in keys. + "context_embedder.bias": "txt_in.bias", + "context_embedder.weight": "txt_in.weight", + # guidance_in MLPEmbedder keys. + "time_text_embed.guidance_embedder.linear_1.bias": "guidance_in.in_layer.bias", + "time_text_embed.guidance_embedder.linear_1.weight": "guidance_in.in_layer.weight", + "time_text_embed.guidance_embedder.linear_2.bias": "guidance_in.out_layer.bias", + "time_text_embed.guidance_embedder.linear_2.weight": "guidance_in.out_layer.weight", + # vector_in MLPEmbedder keys. + "time_text_embed.text_embedder.linear_1.bias": "vector_in.in_layer.bias", + "time_text_embed.text_embedder.linear_1.weight": "vector_in.in_layer.weight", + "time_text_embed.text_embedder.linear_2.bias": "vector_in.out_layer.bias", + "time_text_embed.text_embedder.linear_2.weight": "vector_in.out_layer.weight", + # time_in MLPEmbedder keys. + "time_text_embed.timestep_embedder.linear_1.bias": "time_in.in_layer.bias", + "time_text_embed.timestep_embedder.linear_1.weight": "time_in.in_layer.weight", + "time_text_embed.timestep_embedder.linear_2.bias": "time_in.out_layer.bias", + "time_text_embed.timestep_embedder.linear_2.weight": "time_in.out_layer.weight", + # img_in keys. + "x_embedder.bias": "img_in.bias", + "x_embedder.weight": "img_in.weight", + } + for old_key, new_key in basic_key_map.items(): + v = sd.pop(old_key, None) + if v is not None: + new_sd[new_key] = v + + # Handle the double_blocks. + block_index = 0 + while True: + converted_double_block_sd = _convert_flux_double_block_sd_from_diffusers_to_bfl_format(sd, block_index) + if len(converted_double_block_sd) == 0: + break + new_sd.update(converted_double_block_sd) + block_index += 1 + + # Handle the single_blocks. + block_index = 0 + while True: + converted_singe_block_sd = _convert_flux_single_block_sd_from_diffusers_to_bfl_format(sd, block_index) + if len(converted_singe_block_sd) == 0: + break + new_sd.update(converted_singe_block_sd) + block_index += 1 + + # Transfer controlnet keys as-is. + for k in list(sd.keys()): + if k.startswith("controlnet_"): + new_sd[k] = sd.pop(k) + + # Assert that all keys have been handled. + assert len(sd) == 0 + return new_sd + + +def infer_flux_params_from_state_dict(sd: Dict[str, torch.Tensor]) -> FluxParams: + """Infer the FluxParams from the shape of a FLUX state dict. When a model is distributed in diffusers format, this + information is all contained in the config.json file that accompanies the model. However, being apple to infer the + params from the state dict enables us to load models (e.g. an InstantX ControlNet) from a single weight file. + """ + hidden_size = sd["img_in.weight"].shape[0] + mlp_hidden_dim = sd["double_blocks.0.img_mlp.0.weight"].shape[0] + # mlp_ratio is a float, but we treat it as an int here to avoid having to think about possible float precision + # issues. In practice, mlp_ratio is usually 4. + mlp_ratio = mlp_hidden_dim // hidden_size + + head_dim = sd["double_blocks.0.img_attn.norm.query_norm.scale"].shape[0] + num_heads = hidden_size // head_dim + + # Count the number of double blocks. + double_block_index = 0 + while f"double_blocks.{double_block_index}.img_attn.qkv.weight" in sd: + double_block_index += 1 + + # Count the number of single blocks. + single_block_index = 0 + while f"single_blocks.{single_block_index}.linear1.weight" in sd: + single_block_index += 1 + + return FluxParams( + in_channels=sd["img_in.weight"].shape[1], + vec_in_dim=sd["vector_in.in_layer.weight"].shape[1], + context_in_dim=sd["txt_in.weight"].shape[1], + hidden_size=hidden_size, + mlp_ratio=mlp_ratio, + num_heads=num_heads, + depth=double_block_index, + depth_single_blocks=single_block_index, + # axes_dim cannot be inferred from the state dict. The hard-coded value is correct for dev/schnell models. + axes_dim=[16, 56, 56], + # theta cannot be inferred from the state dict. The hard-coded value is correct for dev/schnell models. + theta=10_000, + qkv_bias="double_blocks.0.img_attn.qkv.bias" in sd, + guidance_embed="guidance_in.in_layer.weight" in sd, + ) + + +def infer_instantx_num_control_modes_from_state_dict(sd: Dict[str, torch.Tensor]) -> int | None: + """Infer the number of ControlNet Union modes from the shape of a InstantX ControlNet state dict. + + Returns None if the model is not a ControlNet Union model. Otherwise returns the number of modes. + """ + mode_embedder_key = "controlnet_mode_embedder.weight" + if mode_embedder_key not in sd: + return None + + return sd[mode_embedder_key].shape[0] diff --git a/invokeai/backend/flux/controlnet/xlabs_controlnet_flux.py b/invokeai/backend/flux/controlnet/xlabs_controlnet_flux.py new file mode 100644 index 00000000000..c7d3a4675d0 --- /dev/null +++ b/invokeai/backend/flux/controlnet/xlabs_controlnet_flux.py @@ -0,0 +1,130 @@ +# This file was initially based on: +# https://github.com/XLabs-AI/x-flux/blob/47495425dbed499be1e8e5a6e52628b07349cba2/src/flux/controlnet.py + + +from dataclasses import dataclass + +import torch +from einops import rearrange + +from invokeai.backend.flux.controlnet.zero_module import zero_module +from invokeai.backend.flux.model import FluxParams +from invokeai.backend.flux.modules.layers import DoubleStreamBlock, EmbedND, MLPEmbedder, timestep_embedding + + +@dataclass +class XLabsControlNetFluxOutput: + controlnet_double_block_residuals: list[torch.Tensor] | None + + +class XLabsControlNetFlux(torch.nn.Module): + """A ControlNet model for FLUX. + + The architecture is very similar to the base FLUX model, with the following differences: + - A `controlnet_depth` parameter is passed to control the number of double_blocks that the ControlNet is applied to. + In order to keep the ControlNet small, this is typically much less than the depth of the base FLUX model. + - There is a set of `controlnet_blocks` that are applied to the output of each double_block. + """ + + def __init__(self, params: FluxParams, controlnet_depth: int = 2): + super().__init__() + + self.params = params + self.in_channels = params.in_channels + self.out_channels = self.in_channels + if params.hidden_size % params.num_heads != 0: + raise ValueError(f"Hidden size {params.hidden_size} must be divisible by num_heads {params.num_heads}") + pe_dim = params.hidden_size // params.num_heads + if sum(params.axes_dim) != pe_dim: + raise ValueError(f"Got {params.axes_dim} but expected positional dim {pe_dim}") + self.hidden_size = params.hidden_size + self.num_heads = params.num_heads + self.pe_embedder = EmbedND(dim=pe_dim, theta=params.theta, axes_dim=params.axes_dim) + self.img_in = torch.nn.Linear(self.in_channels, self.hidden_size, bias=True) + self.time_in = MLPEmbedder(in_dim=256, hidden_dim=self.hidden_size) + self.vector_in = MLPEmbedder(params.vec_in_dim, self.hidden_size) + self.guidance_in = ( + MLPEmbedder(in_dim=256, hidden_dim=self.hidden_size) if params.guidance_embed else torch.nn.Identity() + ) + self.txt_in = torch.nn.Linear(params.context_in_dim, self.hidden_size) + + self.double_blocks = torch.nn.ModuleList( + [ + DoubleStreamBlock( + self.hidden_size, + self.num_heads, + mlp_ratio=params.mlp_ratio, + qkv_bias=params.qkv_bias, + ) + for _ in range(controlnet_depth) + ] + ) + + # Add ControlNet blocks. + self.controlnet_blocks = torch.nn.ModuleList([]) + for _ in range(controlnet_depth): + controlnet_block = torch.nn.Linear(self.hidden_size, self.hidden_size) + controlnet_block = zero_module(controlnet_block) + self.controlnet_blocks.append(controlnet_block) + self.pos_embed_input = torch.nn.Linear(self.in_channels, self.hidden_size, bias=True) + self.input_hint_block = torch.nn.Sequential( + torch.nn.Conv2d(3, 16, 3, padding=1), + torch.nn.SiLU(), + torch.nn.Conv2d(16, 16, 3, padding=1), + torch.nn.SiLU(), + torch.nn.Conv2d(16, 16, 3, padding=1, stride=2), + torch.nn.SiLU(), + torch.nn.Conv2d(16, 16, 3, padding=1), + torch.nn.SiLU(), + torch.nn.Conv2d(16, 16, 3, padding=1, stride=2), + torch.nn.SiLU(), + torch.nn.Conv2d(16, 16, 3, padding=1), + torch.nn.SiLU(), + torch.nn.Conv2d(16, 16, 3, padding=1, stride=2), + torch.nn.SiLU(), + zero_module(torch.nn.Conv2d(16, 16, 3, padding=1)), + ) + + def forward( + self, + img: torch.Tensor, + img_ids: torch.Tensor, + controlnet_cond: torch.Tensor, + txt: torch.Tensor, + txt_ids: torch.Tensor, + timesteps: torch.Tensor, + y: torch.Tensor, + guidance: torch.Tensor | None = None, + ) -> XLabsControlNetFluxOutput: + if img.ndim != 3 or txt.ndim != 3: + raise ValueError("Input img and txt tensors must have 3 dimensions.") + + # running on sequences img + img = self.img_in(img) + controlnet_cond = self.input_hint_block(controlnet_cond) + controlnet_cond = rearrange(controlnet_cond, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=2, pw=2) + controlnet_cond = self.pos_embed_input(controlnet_cond) + img = img + controlnet_cond + vec = self.time_in(timestep_embedding(timesteps, 256)) + if self.params.guidance_embed: + if guidance is None: + raise ValueError("Didn't get guidance strength for guidance distilled model.") + vec = vec + self.guidance_in(timestep_embedding(guidance, 256)) + vec = vec + self.vector_in(y) + txt = self.txt_in(txt) + + ids = torch.cat((txt_ids, img_ids), dim=1) + pe = self.pe_embedder(ids) + + block_res_samples: list[torch.Tensor] = [] + + for block in self.double_blocks: + img, txt = block(img=img, txt=txt, vec=vec, pe=pe) + block_res_samples.append(img) + + controlnet_block_res_samples: list[torch.Tensor] = [] + for block_res_sample, controlnet_block in zip(block_res_samples, self.controlnet_blocks, strict=True): + block_res_sample = controlnet_block(block_res_sample) + controlnet_block_res_samples.append(block_res_sample) + + return XLabsControlNetFluxOutput(controlnet_double_block_residuals=controlnet_block_res_samples) diff --git a/invokeai/backend/flux/controlnet/zero_module.py b/invokeai/backend/flux/controlnet/zero_module.py new file mode 100644 index 00000000000..53a21861a93 --- /dev/null +++ b/invokeai/backend/flux/controlnet/zero_module.py @@ -0,0 +1,12 @@ +from typing import TypeVar + +import torch + +T = TypeVar("T", bound=torch.nn.Module) + + +def zero_module(module: T) -> T: + """Initialize the parameters of a module to zero.""" + for p in module.parameters(): + torch.nn.init.zeros_(p) + return module diff --git a/invokeai/backend/flux/custom_block_processor.py b/invokeai/backend/flux/custom_block_processor.py new file mode 100644 index 00000000000..0f56adacded --- /dev/null +++ b/invokeai/backend/flux/custom_block_processor.py @@ -0,0 +1,138 @@ +import einops +import torch + +from invokeai.backend.flux.extensions.regional_prompting_extension import RegionalPromptingExtension +from invokeai.backend.flux.extensions.xlabs_ip_adapter_extension import XLabsIPAdapterExtension +from invokeai.backend.flux.math import attention +from invokeai.backend.flux.modules.layers import DoubleStreamBlock, SingleStreamBlock + + +class CustomDoubleStreamBlockProcessor: + """A class containing a custom implementation of DoubleStreamBlock.forward() with additional features + (IP-Adapter, etc.). + """ + + @staticmethod + def _double_stream_block_forward( + block: DoubleStreamBlock, + img: torch.Tensor, + txt: torch.Tensor, + vec: torch.Tensor, + pe: torch.Tensor, + attn_mask: torch.Tensor | None = None, + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """This function is a direct copy of DoubleStreamBlock.forward(), but it returns some of the intermediate + values. + """ + img_mod1, img_mod2 = block.img_mod(vec) + txt_mod1, txt_mod2 = block.txt_mod(vec) + + # prepare image for attention + img_modulated = block.img_norm1(img) + img_modulated = (1 + img_mod1.scale) * img_modulated + img_mod1.shift + img_qkv = block.img_attn.qkv(img_modulated) + img_q, img_k, img_v = einops.rearrange(img_qkv, "B L (K H D) -> K B H L D", K=3, H=block.num_heads) + img_q, img_k = block.img_attn.norm(img_q, img_k, img_v) + + # prepare txt for attention + txt_modulated = block.txt_norm1(txt) + txt_modulated = (1 + txt_mod1.scale) * txt_modulated + txt_mod1.shift + txt_qkv = block.txt_attn.qkv(txt_modulated) + txt_q, txt_k, txt_v = einops.rearrange(txt_qkv, "B L (K H D) -> K B H L D", K=3, H=block.num_heads) + txt_q, txt_k = block.txt_attn.norm(txt_q, txt_k, txt_v) + + # run actual attention + q = torch.cat((txt_q, img_q), dim=2) + k = torch.cat((txt_k, img_k), dim=2) + v = torch.cat((txt_v, img_v), dim=2) + + attn = attention(q, k, v, pe=pe, attn_mask=attn_mask) + txt_attn, img_attn = attn[:, : txt.shape[1]], attn[:, txt.shape[1] :] + + # calculate the img bloks + img = img + img_mod1.gate * block.img_attn.proj(img_attn) + img = img + img_mod2.gate * block.img_mlp((1 + img_mod2.scale) * block.img_norm2(img) + img_mod2.shift) + + # calculate the txt bloks + txt = txt + txt_mod1.gate * block.txt_attn.proj(txt_attn) + txt = txt + txt_mod2.gate * block.txt_mlp((1 + txt_mod2.scale) * block.txt_norm2(txt) + txt_mod2.shift) + return img, txt, img_q + + @staticmethod + def custom_double_block_forward( + timestep_index: int, + total_num_timesteps: int, + block_index: int, + block: DoubleStreamBlock, + img: torch.Tensor, + txt: torch.Tensor, + vec: torch.Tensor, + pe: torch.Tensor, + ip_adapter_extensions: list[XLabsIPAdapterExtension], + regional_prompting_extension: RegionalPromptingExtension, + ) -> tuple[torch.Tensor, torch.Tensor]: + """A custom implementation of DoubleStreamBlock.forward() with additional features: + - IP-Adapter support + """ + attn_mask = regional_prompting_extension.get_double_stream_attn_mask(block_index) + img, txt, img_q = CustomDoubleStreamBlockProcessor._double_stream_block_forward( + block, img, txt, vec, pe, attn_mask=attn_mask + ) + + # Apply IP-Adapter conditioning. + for ip_adapter_extension in ip_adapter_extensions: + img = ip_adapter_extension.run_ip_adapter( + timestep_index=timestep_index, + total_num_timesteps=total_num_timesteps, + block_index=block_index, + block=block, + img_q=img_q, + img=img, + ) + + return img, txt + + +class CustomSingleStreamBlockProcessor: + """A class containing a custom implementation of SingleStreamBlock.forward() with additional features (masking, + etc.) + """ + + @staticmethod + def _single_stream_block_forward( + block: SingleStreamBlock, + x: torch.Tensor, + vec: torch.Tensor, + pe: torch.Tensor, + attn_mask: torch.Tensor | None = None, + ) -> torch.Tensor: + """This function is a direct copy of SingleStreamBlock.forward().""" + mod, _ = block.modulation(vec) + x_mod = (1 + mod.scale) * block.pre_norm(x) + mod.shift + qkv, mlp = torch.split(block.linear1(x_mod), [3 * block.hidden_size, block.mlp_hidden_dim], dim=-1) + + q, k, v = einops.rearrange(qkv, "B L (K H D) -> K B H L D", K=3, H=block.num_heads) + q, k = block.norm(q, k, v) + + # compute attention + attn = attention(q, k, v, pe=pe, attn_mask=attn_mask) + # compute activation in mlp stream, cat again and run second linear layer + output = block.linear2(torch.cat((attn, block.mlp_act(mlp)), 2)) + return x + mod.gate * output + + @staticmethod + def custom_single_block_forward( + timestep_index: int, + total_num_timesteps: int, + block_index: int, + block: SingleStreamBlock, + img: torch.Tensor, + vec: torch.Tensor, + pe: torch.Tensor, + regional_prompting_extension: RegionalPromptingExtension, + ) -> torch.Tensor: + """A custom implementation of SingleStreamBlock.forward() with additional features: + - Masking + """ + attn_mask = regional_prompting_extension.get_single_stream_attn_mask(block_index) + return CustomSingleStreamBlockProcessor._single_stream_block_forward(block, img, vec, pe, attn_mask=attn_mask) diff --git a/invokeai/backend/flux/denoise.py b/invokeai/backend/flux/denoise.py new file mode 100644 index 00000000000..0f4cf07ee5b --- /dev/null +++ b/invokeai/backend/flux/denoise.py @@ -0,0 +1,408 @@ +import inspect +import math +from typing import Callable + +import torch +from diffusers.schedulers.scheduling_utils import SchedulerMixin +from tqdm import tqdm + +from invokeai.backend.flux.controlnet.controlnet_flux_output import ControlNetFluxOutput, sum_controlnet_flux_outputs +from invokeai.backend.flux.extensions.dype_extension import DyPEExtension +from invokeai.backend.flux.extensions.instantx_controlnet_extension import InstantXControlNetExtension +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.model import Flux +from invokeai.backend.rectified_flow.rectified_flow_inpaint_extension import RectifiedFlowInpaintExtension +from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState + + +def denoise( + model: Flux, + # model input + img: torch.Tensor, + img_ids: torch.Tensor, + pos_regional_prompting_extension: RegionalPromptingExtension, + neg_regional_prompting_extension: RegionalPromptingExtension | None, + # sampling parameters + timesteps: list[float], + step_callback: Callable[[PipelineIntermediateState], None], + guidance: float, + cfg_scale: list[float], + inpaint_extension: RectifiedFlowInpaintExtension | None, + controlnet_extensions: list[XLabsControlNetExtension | InstantXControlNetExtension], + pos_ip_adapter_extensions: list[XLabsIPAdapterExtension], + neg_ip_adapter_extensions: list[XLabsIPAdapterExtension], + # extra img tokens (channel-wise) + img_cond: torch.Tensor | None, + # extra img tokens (sequence-wise) - for Kontext conditioning + img_cond_seq: torch.Tensor | None = None, + img_cond_seq_ids: torch.Tensor | None = None, + # DyPE extension for high-resolution generation + dype_extension: DyPEExtension | None = None, + # Optional scheduler for alternative sampling methods + scheduler: SchedulerMixin | None = None, +): + # Determine if we're using a diffusers scheduler or the built-in Euler method + use_scheduler = scheduler is not None + + if use_scheduler: + # Initialize scheduler with timesteps + # The timesteps list contains values in [0, 1] range (sigmas) + # LCM should use num_inference_steps (it has its own sigma schedule), + # while other schedulers can use custom sigmas if supported + is_lcm = scheduler.__class__.__name__ == "FlowMatchLCMScheduler" + set_timesteps_sig = inspect.signature(scheduler.set_timesteps) + if not is_lcm and "sigmas" in set_timesteps_sig.parameters: + # Scheduler supports custom sigmas - use InvokeAI's time-shifted schedule + scheduler.set_timesteps(sigmas=timesteps, device=img.device) + else: + # LCM or scheduler doesn't support custom sigmas - use num_inference_steps + # The schedule will be computed by the scheduler itself. + # + # Important for img2img callers: if the initial latent/noise blend was + # computed from a separate pre-scheduler schedule, that preblend may not + # match this scheduler's true first step exactly. + num_inference_steps = len(timesteps) - 1 + scheduler.set_timesteps(num_inference_steps=num_inference_steps, device=img.device) + + # For schedulers like Heun, the number of actual steps may differ + # (Heun doubles timesteps internally) + num_scheduler_steps = len(scheduler.timesteps) + # For user-facing step count, use the original number of denoising steps + total_steps = len(timesteps) - 1 + else: + total_steps = len(timesteps) - 1 + num_scheduler_steps = total_steps + + # guidance_vec is ignored for schnell. + guidance_vec = torch.full((img.shape[0],), guidance, device=img.device, dtype=img.dtype) + + # Store original sequence length for slicing predictions + original_seq_len = img.shape[1] + + # DyPE: Patch model with DyPE-aware position embedder + dype_embedder = None + original_pe_embedder = None + if dype_extension is not None: + dype_embedder, original_pe_embedder = dype_extension.patch_model(model) + + try: + # Track the actual step for user-facing progress (accounts for Heun's double steps) + user_step = 0 + + if use_scheduler: + # 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): + timestep = scheduler.timesteps[step_index] + # Convert scheduler timestep (0-1000) to normalized (0-1) for the model + t_curr = timestep.item() / scheduler.config.num_train_timesteps + dype_sigma = DyPEExtension.resolve_step_sigma( + fallback_sigma=t_curr, + step_index=step_index, + scheduler_sigmas=getattr(scheduler, "sigmas", None), + ) + t_vec = torch.full((img.shape[0],), t_curr, dtype=img.dtype, device=img.device) + + # DyPE: Update step state for timestep-dependent scaling + if dype_extension is not None and dype_embedder is not None: + dype_extension.update_step_state( + embedder=dype_embedder, + sigma=dype_sigma, + ) + + # 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 + + # Run ControlNet models + controlnet_residuals: list[ControlNetFluxOutput] = [] + for controlnet_extension in controlnet_extensions: + controlnet_residuals.append( + controlnet_extension.run_controlnet( + timestep_index=user_step, + total_num_timesteps=total_steps, + img=img, + img_ids=img_ids, + txt=pos_regional_prompting_extension.regional_text_conditioning.t5_embeddings, + txt_ids=pos_regional_prompting_extension.regional_text_conditioning.t5_txt_ids, + y=pos_regional_prompting_extension.regional_text_conditioning.clip_embeddings, + timesteps=t_vec, + guidance=guidance_vec, + ) + ) + + merged_controlnet_residuals = sum_controlnet_flux_outputs(controlnet_residuals) + + # Prepare input for model + img_input = img + img_input_ids = img_ids + + if img_cond is not None: + img_input = torch.cat((img_input, img_cond), dim=-1) + + if img_cond_seq is not None: + assert img_cond_seq_ids is not None + img_input = torch.cat((img_input, img_cond_seq), dim=1) + img_input_ids = torch.cat((img_input_ids, img_cond_seq_ids), dim=1) + + pred = model( + img=img_input, + img_ids=img_input_ids, + txt=pos_regional_prompting_extension.regional_text_conditioning.t5_embeddings, + txt_ids=pos_regional_prompting_extension.regional_text_conditioning.t5_txt_ids, + y=pos_regional_prompting_extension.regional_text_conditioning.clip_embeddings, + timesteps=t_vec, + guidance=guidance_vec, + timestep_index=user_step, + total_num_timesteps=total_steps, + controlnet_double_block_residuals=merged_controlnet_residuals.double_block_residuals, + controlnet_single_block_residuals=merged_controlnet_residuals.single_block_residuals, + ip_adapter_extensions=pos_ip_adapter_extensions, + regional_prompting_extension=pos_regional_prompting_extension, + ) + + if img_cond_seq is not None: + pred = pred[:, :original_seq_len] + + # Get CFG scale for current user step + step_cfg_scale = cfg_scale[min(user_step, len(cfg_scale) - 1)] + + if not math.isclose(step_cfg_scale, 1.0): + if neg_regional_prompting_extension is None: + raise ValueError("Negative text conditioning is required when cfg_scale is not 1.0.") + + neg_img_input = img + neg_img_input_ids = img_ids + + if img_cond is not None: + neg_img_input = torch.cat((neg_img_input, img_cond), dim=-1) + + if img_cond_seq is not None: + neg_img_input = torch.cat((neg_img_input, img_cond_seq), dim=1) + neg_img_input_ids = torch.cat((neg_img_input_ids, img_cond_seq_ids), dim=1) + + neg_pred = model( + img=neg_img_input, + img_ids=neg_img_input_ids, + txt=neg_regional_prompting_extension.regional_text_conditioning.t5_embeddings, + txt_ids=neg_regional_prompting_extension.regional_text_conditioning.t5_txt_ids, + y=neg_regional_prompting_extension.regional_text_conditioning.clip_embeddings, + timesteps=t_vec, + guidance=guidance_vec, + timestep_index=user_step, + total_num_timesteps=total_steps, + controlnet_double_block_residuals=None, + controlnet_single_block_residuals=None, + ip_adapter_extensions=neg_ip_adapter_extensions, + regional_prompting_extension=neg_regional_prompting_extension, + ) + + if img_cond_seq is not None: + neg_pred = neg_pred[:, :original_seq_len] + pred = neg_pred + step_cfg_scale * (pred - neg_pred) + + # Use scheduler.step() for the update + step_output = scheduler.step(model_output=pred, timestep=timestep, sample=img) + img = step_output.prev_sample + + # Get t_prev for inpainting (next sigma value) + if step_index + 1 < len(scheduler.sigmas): + t_prev = scheduler.sigmas[step_index + 1].item() + else: + t_prev = 0.0 + + if inpaint_extension is not None: + img = inpaint_extension.merge_intermediate_latents_with_init_latents(img, t_prev) + + # For Heun, only increment user step after second-order step completes + if is_heun: + if not in_first_order: + # Second order step completed + user_step += 1 + # Only call step_callback if we haven't exceeded total_steps + if user_step <= total_steps: + pbar.update(1) + preview_img = img - t_curr * pred + if inpaint_extension is not None: + preview_img = inpaint_extension.merge_intermediate_latents_with_init_latents( + preview_img, 0.0 + ) + step_callback( + PipelineIntermediateState( + step=user_step, + order=2, + total_steps=total_steps, + timestep=int(t_curr * 1000), + latents=preview_img, + ), + ) + else: + # For LCM and other first-order schedulers + user_step += 1 + # Only call step_callback if we haven't exceeded total_steps + # (LCM scheduler may have more internal steps than user-facing steps) + if user_step <= total_steps: + pbar.update(1) + preview_img = img - t_curr * pred + if inpaint_extension is not None: + preview_img = inpaint_extension.merge_intermediate_latents_with_init_latents( + preview_img, 0.0 + ) + step_callback( + PipelineIntermediateState( + step=user_step, + order=1, + total_steps=total_steps, + timestep=int(t_curr * 1000), + latents=preview_img, + ), + ) + + pbar.close() + return img + + # Original Euler implementation (when scheduler is None) + for step_index, (t_curr, t_prev) in tqdm(list(enumerate(zip(timesteps[:-1], timesteps[1:], strict=True)))): + # DyPE: Update step state for timestep-dependent scaling + if dype_extension is not None and dype_embedder is not None: + dype_extension.update_step_state( + embedder=dype_embedder, + sigma=t_curr, + ) + + t_vec = torch.full((img.shape[0],), t_curr, dtype=img.dtype, device=img.device) + + # Run ControlNet models. + controlnet_residuals: list[ControlNetFluxOutput] = [] + for controlnet_extension in controlnet_extensions: + controlnet_residuals.append( + controlnet_extension.run_controlnet( + timestep_index=step_index, + total_num_timesteps=total_steps, + img=img, + img_ids=img_ids, + txt=pos_regional_prompting_extension.regional_text_conditioning.t5_embeddings, + txt_ids=pos_regional_prompting_extension.regional_text_conditioning.t5_txt_ids, + y=pos_regional_prompting_extension.regional_text_conditioning.clip_embeddings, + timesteps=t_vec, + guidance=guidance_vec, + ) + ) + + # Merge the ControlNet residuals from multiple ControlNets. + # TODO(ryand): We may want to calculate the sum just-in-time to keep peak memory low. Keep in mind, that the + # controlnet_residuals datastructure is efficient in that it likely contains multiple references to the same + # tensors. Calculating the sum materializes each tensor into its own instance. + merged_controlnet_residuals = sum_controlnet_flux_outputs(controlnet_residuals) + + # Prepare input for model - concatenate fresh each step + img_input = img + img_input_ids = img_ids + + # Add channel-wise conditioning (for ControlNet, FLUX Fill, etc.) + if img_cond is not None: + img_input = torch.cat((img_input, img_cond), dim=-1) + + # Add sequence-wise conditioning (for Kontext) + if img_cond_seq is not None: + assert img_cond_seq_ids is not None, ( + "You need to provide either both or neither of the sequence conditioning" + ) + img_input = torch.cat((img_input, img_cond_seq), dim=1) + img_input_ids = torch.cat((img_input_ids, img_cond_seq_ids), dim=1) + + pred = model( + img=img_input, + img_ids=img_input_ids, + txt=pos_regional_prompting_extension.regional_text_conditioning.t5_embeddings, + txt_ids=pos_regional_prompting_extension.regional_text_conditioning.t5_txt_ids, + y=pos_regional_prompting_extension.regional_text_conditioning.clip_embeddings, + timesteps=t_vec, + guidance=guidance_vec, + timestep_index=step_index, + total_num_timesteps=total_steps, + controlnet_double_block_residuals=merged_controlnet_residuals.double_block_residuals, + controlnet_single_block_residuals=merged_controlnet_residuals.single_block_residuals, + ip_adapter_extensions=pos_ip_adapter_extensions, + regional_prompting_extension=pos_regional_prompting_extension, + ) + + # Slice prediction to only include the main image tokens + if img_cond_seq is not None: + pred = pred[:, :original_seq_len] + + step_cfg_scale = cfg_scale[step_index] + + # If step_cfg_scale, is 1.0, then we don't need to run the negative prediction. + if not math.isclose(step_cfg_scale, 1.0): + # TODO(ryand): Add option to run positive and negative predictions in a single batch for better performance + # on systems with sufficient VRAM. + + if neg_regional_prompting_extension is None: + raise ValueError("Negative text conditioning is required when cfg_scale is not 1.0.") + + # For negative prediction with Kontext, we need to include the reference images + # to maintain consistency between positive and negative passes. Without this, + # CFG would create artifacts as the attention mechanism would see different + # spatial structures in each pass + neg_img_input = img + neg_img_input_ids = img_ids + + # Add channel-wise conditioning for negative pass if present + if img_cond is not None: + neg_img_input = torch.cat((neg_img_input, img_cond), dim=-1) + + # Add sequence-wise conditioning (Kontext) for negative pass + # This ensures reference images are processed consistently + if img_cond_seq is not None: + neg_img_input = torch.cat((neg_img_input, img_cond_seq), dim=1) + neg_img_input_ids = torch.cat((neg_img_input_ids, img_cond_seq_ids), dim=1) + + neg_pred = model( + img=neg_img_input, + img_ids=neg_img_input_ids, + txt=neg_regional_prompting_extension.regional_text_conditioning.t5_embeddings, + txt_ids=neg_regional_prompting_extension.regional_text_conditioning.t5_txt_ids, + y=neg_regional_prompting_extension.regional_text_conditioning.clip_embeddings, + timesteps=t_vec, + guidance=guidance_vec, + timestep_index=step_index, + total_num_timesteps=total_steps, + controlnet_double_block_residuals=None, + controlnet_single_block_residuals=None, + ip_adapter_extensions=neg_ip_adapter_extensions, + regional_prompting_extension=neg_regional_prompting_extension, + ) + + # Slice negative prediction to match main image tokens + if img_cond_seq is not None: + neg_pred = neg_pred[:, :original_seq_len] + pred = neg_pred + step_cfg_scale * (pred - neg_pred) + + preview_img = img - t_curr * pred + img = img + (t_prev - t_curr) * pred + + if inpaint_extension is not None: + img = inpaint_extension.merge_intermediate_latents_with_init_latents(img, t_prev) + preview_img = inpaint_extension.merge_intermediate_latents_with_init_latents(preview_img, 0.0) + + step_callback( + PipelineIntermediateState( + step=step_index + 1, + order=1, + total_steps=total_steps, + timestep=int(t_curr), + latents=preview_img, + ), + ) + + return img + + finally: + # DyPE: Restore original position embedder + if original_pe_embedder is not None: + DyPEExtension.restore_model(model, original_pe_embedder) diff --git a/invokeai/backend/flux/dype/__init__.py b/invokeai/backend/flux/dype/__init__.py new file mode 100644 index 00000000000..7af50625dd7 --- /dev/null +++ b/invokeai/backend/flux/dype/__init__.py @@ -0,0 +1,35 @@ +"""Dynamic Position Extrapolation (DyPE) for FLUX models. + +DyPE enables high-resolution image generation with pretrained FLUX models by +dynamically modulating RoPE extrapolation during denoising. + +Based on the official DyPE project: https://github.com/guyyariv/DyPE +""" + +from invokeai.backend.flux.dype.base import DyPEConfig +from invokeai.backend.flux.dype.embed import DyPEEmbedND +from invokeai.backend.flux.dype.presets import ( + DYPE_PRESET_4K, + DYPE_PRESET_AREA, + DYPE_PRESET_AUTO, + DYPE_PRESET_LABELS, + DYPE_PRESET_MANUAL, + DYPE_PRESET_OFF, + DyPEPreset, + get_dype_config_for_area, + get_dype_config_for_resolution, +) + +__all__ = [ + "DyPEConfig", + "DyPEEmbedND", + "DyPEPreset", + "DYPE_PRESET_OFF", + "DYPE_PRESET_MANUAL", + "DYPE_PRESET_AUTO", + "DYPE_PRESET_AREA", + "DYPE_PRESET_4K", + "DYPE_PRESET_LABELS", + "get_dype_config_for_area", + "get_dype_config_for_resolution", +] diff --git a/invokeai/backend/flux/dype/base.py b/invokeai/backend/flux/dype/base.py new file mode 100644 index 00000000000..6c3fc42fa2c --- /dev/null +++ b/invokeai/backend/flux/dype/base.py @@ -0,0 +1,115 @@ +"""DyPE base configuration and utilities for FLUX vision_yarn RoPE.""" + +from dataclasses import dataclass + +import torch +from torch import Tensor + + +@dataclass +class DyPEConfig: + """Configuration for Dynamic Position Extrapolation.""" + + enable_dype: bool = True + base_resolution: int = 1024 # Native training resolution + dype_scale: float = 2.0 # Magnitude λs (0.0-8.0) + dype_exponent: float = 2.0 # Decay speed λt (0.0-1000.0) + dype_start_sigma: float = 1.0 # When DyPE decay starts + + +def get_timestep_kappa( + current_sigma: float, + dype_scale: float, + dype_exponent: float, + dype_start_sigma: float, +) -> float: + """Calculate the paper-style DyPE scheduler value κ(t). + + The key insight of DyPE: early steps focus on low frequencies (global structure), + late steps on high frequencies (details). DyPE expresses this as a direct + timestep scheduler over the positional extrapolation strength: + + κ(t) = λs * t^λt + + Args: + current_sigma: Current noise level (1.0 = full noise, 0.0 = clean) + dype_scale: DyPE magnitude (λs) + dype_exponent: DyPE decay speed (λt) + dype_start_sigma: Sigma threshold to start decay + + Returns: + Timestep scheduler value κ(t) + """ + if dype_scale <= 0.0 or dype_start_sigma <= 0.0: + return 0.0 + + t_normalized = max(0.0, min(current_sigma / dype_start_sigma, 1.0)) + return dype_scale * (t_normalized**dype_exponent) + + +def compute_vision_yarn_freqs( + pos: Tensor, + dim: int, + theta: int, + scale_h: float, + scale_w: float, + current_sigma: float, + dype_config: DyPEConfig, +) -> tuple[Tensor, Tensor]: + """Compute RoPE frequencies using NTK-aware scaling for high-resolution. + + This method extends FLUX's position encoding to handle resolutions beyond + the 1024px training resolution by scaling the base frequency (theta). + + The NTK-aware approach smoothly interpolates frequencies to cover larger + position ranges without breaking the attention patterns. + + DyPE (Dynamic Position Extrapolation) modulates the NTK scaling based on + the current timestep - stronger extrapolation in early steps (global structure), + weaker in late steps (fine details). + + Args: + pos: Position tensor + dim: Embedding dimension + theta: RoPE base frequency + scale_h: Height scaling factor + scale_w: Width scaling factor + current_sigma: Current noise level (1.0 = full noise, 0.0 = clean) + dype_config: DyPE configuration + + Returns: + Tuple of (cos, sin) frequency tensors + """ + assert dim % 2 == 0 + + scale = max(scale_h, scale_w) + + device = pos.device + dtype = torch.float64 if device.type != "mps" else torch.float32 + + # DyPE applies a direct timestep scheduler to the NTK extrapolation exponent. + # Early steps keep strong extrapolation; late steps relax smoothly back + # toward the training-time RoPE. + if scale > 1.0: + ntk_exponent = dim / (dim - 2) + kappa = get_timestep_kappa( + current_sigma=current_sigma, + dype_scale=dype_config.dype_scale, + dype_exponent=dype_config.dype_exponent, + dype_start_sigma=dype_config.dype_start_sigma, + ) + scaled_theta = theta * (scale ** (ntk_exponent * kappa)) + else: + scaled_theta = theta + + # Standard RoPE frequency computation + freq_seq = torch.arange(0, dim, 2, dtype=dtype, device=device) / dim + freqs = 1.0 / (scaled_theta**freq_seq) + + # Compute angles = position * frequency + angles = torch.einsum("...n,d->...nd", pos.to(dtype), freqs) + + cos = torch.cos(angles) + sin = torch.sin(angles) + + return cos.to(pos.dtype), sin.to(pos.dtype) diff --git a/invokeai/backend/flux/dype/embed.py b/invokeai/backend/flux/dype/embed.py new file mode 100644 index 00000000000..ace6a56ab0f --- /dev/null +++ b/invokeai/backend/flux/dype/embed.py @@ -0,0 +1,116 @@ +"""DyPE-enhanced position embedding module.""" + +import torch +from torch import Tensor, nn + +from invokeai.backend.flux.dype.base import DyPEConfig +from invokeai.backend.flux.dype.rope import rope_dype + + +class DyPEEmbedND(nn.Module): + """N-dimensional position embedding with DyPE support. + + This class replaces the standard EmbedND from FLUX with a DyPE-aware version + that dynamically scales position embeddings based on resolution and timestep. + + The key difference from EmbedND: + - Maintains step state (current_sigma, target dimensions) + - Uses rope_dype() instead of rope() for frequency computation + - Applies timestep-dependent scaling for better high-resolution generation + """ + + def __init__( + self, + dim: int, + theta: int, + axes_dim: list[int], + dype_config: DyPEConfig, + ): + """Initialize DyPE position embedder. + + Args: + dim: Total embedding dimension (sum of axes_dim) + theta: RoPE base frequency + axes_dim: Dimension allocation per axis (e.g., [16, 56, 56] for FLUX) + dype_config: DyPE configuration + """ + super().__init__() + self.dim = dim + self.theta = theta + self.axes_dim = axes_dim + self.dype_config = dype_config + + # Step state - updated before each denoising step + self._current_sigma: float = 1.0 + self._target_height: int = 1024 + self._target_width: int = 1024 + + def set_step_state(self, sigma: float, height: int, width: int) -> None: + """Update the step state before each denoising step. + + This method should be called by the DyPE extension before each step + to update the current noise level and target dimensions. + + Args: + sigma: Current noise level (timestep value, 1.0 = full noise) + height: Target image height in pixels + width: Target image width in pixels + """ + self._current_sigma = sigma + self._target_height = height + self._target_width = width + + def forward(self, ids: Tensor) -> Tensor: + """Compute position embeddings with DyPE scaling. + + Args: + ids: Position indices tensor with shape (batch, seq_len, n_axes) + For FLUX: n_axes=3 (time/channel, height, width) + + Returns: + Position embedding tensor with shape (batch, 1, seq_len, dim) + """ + n_axes = ids.shape[-1] + + # Compute RoPE for each axis with DyPE scaling + embeddings = [] + for i in range(n_axes): + axis_emb = rope_dype( + pos=ids[..., i], + dim=self.axes_dim[i], + theta=self.theta, + current_sigma=self._current_sigma, + target_height=self._target_height, + target_width=self._target_width, + dype_config=self.dype_config, + ) + embeddings.append(axis_emb) + + # Concatenate embeddings from all axes + emb = torch.cat(embeddings, dim=-3) + + return emb.unsqueeze(1) + + @classmethod + def from_embednd( + cls, + embed_nd: nn.Module, + dype_config: DyPEConfig, + ) -> "DyPEEmbedND": + """Create a DyPEEmbedND from an existing EmbedND. + + This is a convenience method for patching an existing FLUX model. + + Args: + embed_nd: Original EmbedND module from FLUX + dype_config: DyPE configuration + + Returns: + New DyPEEmbedND with same parameters + """ + return cls( + dim=embed_nd.dim, + theta=embed_nd.theta, + axes_dim=embed_nd.axes_dim, + dype_config=dype_config, + ) diff --git a/invokeai/backend/flux/dype/presets.py b/invokeai/backend/flux/dype/presets.py new file mode 100644 index 00000000000..48a714b007a --- /dev/null +++ b/invokeai/backend/flux/dype/presets.py @@ -0,0 +1,198 @@ +"""DyPE presets and automatic configuration.""" + +import math +from dataclasses import dataclass +from typing import Literal + +from invokeai.backend.flux.dype.base import DyPEConfig + +# DyPE preset type - using Literal for proper frontend dropdown support +DyPEPreset = Literal["off", "manual", "auto", "area", "4k"] + +# Constants for preset values +DYPE_PRESET_OFF: DyPEPreset = "off" +DYPE_PRESET_MANUAL: DyPEPreset = "manual" +DYPE_PRESET_AUTO: DyPEPreset = "auto" +DYPE_PRESET_AREA: DyPEPreset = "area" +DYPE_PRESET_4K: DyPEPreset = "4k" + +# Human-readable labels for the UI +DYPE_PRESET_LABELS: dict[str, str] = { + "off": "Off", + "manual": "Manual", + "auto": "Auto (>1536px)", + "area": "Area (auto)", + "4k": "4K Optimized", +} + + +@dataclass +class DyPEPresetConfig: + """Preset configuration values.""" + + base_resolution: int + dype_scale: float + dype_exponent: float + dype_start_sigma: float + + +# Predefined preset configurations +DYPE_PRESETS: dict[DyPEPreset, DyPEPresetConfig] = { + DYPE_PRESET_4K: DyPEPresetConfig( + base_resolution=1024, + dype_scale=2.0, + dype_exponent=2.0, + dype_start_sigma=1.0, + ), +} + + +def get_dype_config_for_resolution( + width: int, + height: int, + base_resolution: int = 1024, + activation_threshold: int = 1536, +) -> DyPEConfig | None: + """Automatically determine DyPE config based on target resolution. + + FLUX can handle resolutions up to ~1.5x natively without significant artifacts. + DyPE is only activated when the resolution exceeds the activation threshold. + + Args: + width: Target image width in pixels + height: Target image height in pixels + base_resolution: Native training resolution of the model (for scale calculation) + activation_threshold: Resolution threshold above which DyPE is activated + + Returns: + DyPEConfig if DyPE should be enabled, None otherwise + """ + max_dim = max(width, height) + + if max_dim <= activation_threshold: + return None # FLUX can handle this natively + + # Calculate scaling factor based on base_resolution + scale = max_dim / base_resolution + + # Dynamic parameters based on scaling + # Higher resolution = higher dype_scale, capped at 8.0 + dynamic_dype_scale = min(2.0 * scale, 8.0) + + return DyPEConfig( + enable_dype=True, + base_resolution=base_resolution, + dype_scale=dynamic_dype_scale, + dype_exponent=2.0, + dype_start_sigma=1.0, + ) + + +def get_dype_config_for_area( + width: int, + height: int, + base_resolution: int = 1024, +) -> DyPEConfig | None: + """Automatically determine DyPE config based on target area. + + Uses sqrt(area/base_area) as an effective side-length ratio. + DyPE is enabled only when target area exceeds base area. + + Returns: + DyPEConfig if DyPE should be enabled, None otherwise + """ + area = width * height + base_area = base_resolution**2 + + if area <= base_area: + return None + + area_ratio = area / base_area + effective_side_ratio = math.sqrt(area_ratio) + aspect_ratio = max(width, height) / min(width, height) + aspect_attenuation = 1.0 if aspect_ratio <= 2.0 else 2.0 / aspect_ratio + + # Retune area mode to be "auto, but area-aware" instead of dramatically + # stronger than auto. This keeps it closer to the paper-style core DyPE. + dynamic_dype_scale = 2.4 * effective_side_ratio + dynamic_dype_scale *= aspect_attenuation + dynamic_dype_scale = max(0.0, min(dynamic_dype_scale, 8.0)) + + # Use a narrower, higher exponent range than the old area heuristic so the + # paper-style scheduler decays more conservatively and artifacts are reduced. + exponent_progress = max(0.0, min(effective_side_ratio - 1.0, 1.0)) + dype_exponent = 1.25 + 0.75 * exponent_progress + + return DyPEConfig( + enable_dype=True, + base_resolution=base_resolution, + dype_scale=dynamic_dype_scale, + dype_exponent=dype_exponent, + dype_start_sigma=1.0, + ) + + +def get_dype_config_from_preset( + preset: DyPEPreset, + width: int, + height: int, + custom_scale: float | None = None, + custom_exponent: float | None = None, +) -> DyPEConfig | None: + """Get DyPE configuration from a preset or custom values. + + Args: + preset: The DyPE preset to use + width: Target image width + height: Target image height + custom_scale: Optional custom dype_scale (only used with 'manual' preset) + custom_exponent: Optional custom dype_exponent (only used with 'manual' preset) + + Returns: + DyPEConfig if DyPE should be enabled, None otherwise + """ + if preset == DYPE_PRESET_OFF: + return None + + if preset == DYPE_PRESET_MANUAL: + # Manual mode - custom values can override defaults + max_dim = max(width, height) + scale = max_dim / 1024 + dynamic_dype_scale = min(2.0 * scale, 8.0) + return DyPEConfig( + enable_dype=True, + base_resolution=1024, + dype_scale=custom_scale if custom_scale is not None else dynamic_dype_scale, + dype_exponent=custom_exponent if custom_exponent is not None else 2.0, + dype_start_sigma=1.0, + ) + + if preset == DYPE_PRESET_AUTO: + # Auto preset - custom values are ignored + return get_dype_config_for_resolution( + width=width, + height=height, + base_resolution=1024, + activation_threshold=1536, + ) + + if preset == DYPE_PRESET_AREA: + # Area-based preset - custom values are ignored + return get_dype_config_for_area( + width=width, + height=height, + base_resolution=1024, + ) + + # Use preset configuration (4K etc.) - custom values are ignored + preset_config = DYPE_PRESETS.get(preset) + if preset_config is None: + return None + + return DyPEConfig( + enable_dype=True, + base_resolution=preset_config.base_resolution, + dype_scale=preset_config.dype_scale, + dype_exponent=preset_config.dype_exponent, + dype_start_sigma=preset_config.dype_start_sigma, + ) diff --git a/invokeai/backend/flux/dype/rope.py b/invokeai/backend/flux/dype/rope.py new file mode 100644 index 00000000000..980b768cbc0 --- /dev/null +++ b/invokeai/backend/flux/dype/rope.py @@ -0,0 +1,86 @@ +"""DyPE-enhanced RoPE (Rotary Position Embedding) functions.""" + +import torch +from einops import rearrange +from torch import Tensor + +from invokeai.backend.flux.dype.base import ( + DyPEConfig, + compute_vision_yarn_freqs, +) + + +def rope_dype( + pos: Tensor, + dim: int, + theta: int, + current_sigma: float, + target_height: int, + target_width: int, + dype_config: DyPEConfig, +) -> Tensor: + """Compute RoPE with Dynamic Position Extrapolation. + + This is the core DyPE function that replaces the standard rope() function. + It applies resolution-aware and timestep-aware scaling to position embeddings. + + Args: + pos: Position indices tensor + dim: Embedding dimension per axis + theta: RoPE base frequency (typically 10000) + current_sigma: Current noise level (1.0 = full noise, 0.0 = clean) + target_height: Target image height in pixels + target_width: Target image width in pixels + dype_config: DyPE configuration + + Returns: + Rotary position embedding tensor with shape suitable for FLUX attention + """ + assert dim % 2 == 0 + + # Calculate scaling factors + base_res = dype_config.base_resolution + scale_h = target_height / base_res + scale_w = target_width / base_res + scale = max(scale_h, scale_w) + + # If no scaling needed and DyPE disabled, use base method + if not dype_config.enable_dype or scale <= 1.0: + return _rope_base(pos, dim, theta) + + cos, sin = compute_vision_yarn_freqs( + pos=pos, + dim=dim, + theta=theta, + scale_h=scale_h, + scale_w=scale_w, + current_sigma=current_sigma, + dype_config=dype_config, + ) + + # Construct rotation matrix from cos/sin + # Output shape: (batch, seq_len, dim/2, 2, 2) + out = torch.stack([cos, -sin, sin, cos], dim=-1) + out = rearrange(out, "b n d (i j) -> b n d i j", i=2, j=2) + + return out.to(dtype=pos.dtype, device=pos.device) + + +def _rope_base(pos: Tensor, dim: int, theta: int) -> Tensor: + """Standard RoPE without DyPE scaling. + + This matches the original rope() function from invokeai.backend.flux.math. + """ + assert dim % 2 == 0 + + device = pos.device + dtype = torch.float64 if device.type != "mps" else torch.float32 + + scale = torch.arange(0, dim, 2, dtype=dtype, device=device) / dim + omega = 1.0 / (theta**scale) + + out = torch.einsum("...n,d->...nd", pos.to(dtype), omega) + out = torch.stack([torch.cos(out), -torch.sin(out), torch.sin(out), torch.cos(out)], dim=-1) + out = rearrange(out, "b n d (i j) -> b n d i j", i=2, j=2) + + return out.to(dtype=pos.dtype, device=pos.device) diff --git a/invokeai/backend/flux/extensions/__init__.py b/invokeai/backend/flux/extensions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/flux/extensions/base_controlnet_extension.py b/invokeai/backend/flux/extensions/base_controlnet_extension.py new file mode 100644 index 00000000000..9736aaea5b9 --- /dev/null +++ b/invokeai/backend/flux/extensions/base_controlnet_extension.py @@ -0,0 +1,45 @@ +import math +from abc import ABC, abstractmethod +from typing import List, Union + +import torch + +from invokeai.backend.flux.controlnet.controlnet_flux_output import ControlNetFluxOutput + + +class BaseControlNetExtension(ABC): + def __init__( + self, + weight: Union[float, List[float]], + begin_step_percent: float, + end_step_percent: float, + ): + self._weight = weight + self._begin_step_percent = begin_step_percent + self._end_step_percent = end_step_percent + + def _get_weight(self, timestep_index: int, total_num_timesteps: int) -> float: + first_step = math.floor(self._begin_step_percent * total_num_timesteps) + last_step = math.ceil(self._end_step_percent * total_num_timesteps) + + if timestep_index < first_step or timestep_index > last_step: + return 0.0 + + if isinstance(self._weight, list): + return self._weight[timestep_index] + + return self._weight + + @abstractmethod + def run_controlnet( + self, + timestep_index: int, + total_num_timesteps: int, + img: torch.Tensor, + img_ids: torch.Tensor, + txt: torch.Tensor, + txt_ids: torch.Tensor, + y: torch.Tensor, + timesteps: torch.Tensor, + guidance: torch.Tensor | None, + ) -> ControlNetFluxOutput: ... diff --git a/invokeai/backend/flux/extensions/dype_extension.py b/invokeai/backend/flux/extensions/dype_extension.py new file mode 100644 index 00000000000..af01a305b7b --- /dev/null +++ b/invokeai/backend/flux/extensions/dype_extension.py @@ -0,0 +1,113 @@ +"""DyPE extension for FLUX denoising pipeline.""" + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Sequence + +import torch + +from invokeai.backend.flux.dype.base import DyPEConfig +from invokeai.backend.flux.dype.embed import DyPEEmbedND + +if TYPE_CHECKING: + from invokeai.backend.flux.model import Flux + + +@dataclass +class DyPEExtension: + """Extension for Dynamic Position Extrapolation in FLUX models. + + This extension manages the patching of the FLUX model's position embedder + and updates the step state during denoising. + + Usage: + 1. Create extension with config and target dimensions + 2. Call patch_model() to replace pe_embedder with DyPE version + 3. Call update_step_state() before each denoising step + 4. Call restore_model() after denoising to restore original embedder + """ + + config: DyPEConfig + target_height: int + target_width: int + + def patch_model(self, model: "Flux") -> tuple[DyPEEmbedND, object]: + """Patch the model's position embedder with DyPE version. + + Args: + model: The FLUX model to patch + + Returns: + Tuple of (new DyPE embedder, original embedder for restoration) + """ + original_embedder = model.pe_embedder + + dype_embedder = DyPEEmbedND.from_embednd( + embed_nd=original_embedder, + dype_config=self.config, + ) + + # Set initial state + dype_embedder.set_step_state( + sigma=1.0, + height=self.target_height, + width=self.target_width, + ) + + # Replace the embedder + model.pe_embedder = dype_embedder + + return dype_embedder, original_embedder + + def update_step_state( + self, + embedder: DyPEEmbedND, + sigma: float, + ) -> None: + """Update the step state in the DyPE embedder. + + This should be called before each denoising step to update the + current noise level for timestep-dependent scaling. + + Args: + embedder: The DyPE embedder to update + sigma: Current noise level for the active denoising step + """ + embedder.set_step_state( + sigma=sigma, + height=self.target_height, + width=self.target_width, + ) + + @staticmethod + def resolve_step_sigma( + fallback_sigma: float, + step_index: int, + scheduler_sigmas: Sequence[float] | torch.Tensor | None, + ) -> float: + """Resolve the actual sigma for the current denoising step. + + Diffusers schedulers may expose both normalized timesteps and the underlying + sigma sequence. DyPE should follow the noise schedule, so prefer + ``scheduler.sigmas`` when available and fall back to the provided value + otherwise. + """ + if scheduler_sigmas is None: + return fallback_sigma + + if step_index >= len(scheduler_sigmas): + return fallback_sigma + + sigma = scheduler_sigmas[step_index] + if isinstance(sigma, torch.Tensor): + return float(sigma.item()) + return float(sigma) + + @staticmethod + def restore_model(model: "Flux", original_embedder: object) -> None: + """Restore the original position embedder. + + Args: + model: The FLUX model to restore + original_embedder: The original embedder saved from patch_model() + """ + model.pe_embedder = original_embedder diff --git a/invokeai/backend/flux/extensions/instantx_controlnet_extension.py b/invokeai/backend/flux/extensions/instantx_controlnet_extension.py new file mode 100644 index 00000000000..f03d2d21aa3 --- /dev/null +++ b/invokeai/backend/flux/extensions/instantx_controlnet_extension.py @@ -0,0 +1,194 @@ +import math +from typing import List, Union + +import torch +from PIL.Image import Image + +from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR +from invokeai.app.invocations.flux_vae_encode import FluxVaeEncodeInvocation +from invokeai.app.util.controlnet_utils import CONTROLNET_RESIZE_VALUES, prepare_control_image +from invokeai.backend.flux.controlnet.controlnet_flux_output import ControlNetFluxOutput +from invokeai.backend.flux.controlnet.instantx_controlnet_flux import ( + InstantXControlNetFlux, + InstantXControlNetFluxOutput, +) +from invokeai.backend.flux.extensions.base_controlnet_extension import BaseControlNetExtension +from invokeai.backend.flux.sampling_utils import pack +from invokeai.backend.model_manager.load.load_base import LoadedModel + + +class InstantXControlNetExtension(BaseControlNetExtension): + def __init__( + self, + model: InstantXControlNetFlux, + controlnet_cond: torch.Tensor, + instantx_control_mode: torch.Tensor | None, + weight: Union[float, List[float]], + begin_step_percent: float, + end_step_percent: float, + ): + super().__init__( + weight=weight, + begin_step_percent=begin_step_percent, + end_step_percent=end_step_percent, + ) + self._model = model + # The VAE-encoded and 'packed' control image to pass to the ControlNet model. + self._controlnet_cond = controlnet_cond + # TODO(ryand): Should we define an enum for the instantx_control_mode? Is it likely to change for future models? + # The control mode for InstantX ControlNet union models. + # See the values defined here: https://huggingface.co/InstantX/FLUX.1-dev-Controlnet-Union#control-mode + # Expected shape: (batch_size, 1), Expected dtype: torch.long + # If None, a zero-embedding will be used. + self._instantx_control_mode = instantx_control_mode + + # TODO(ryand): Pass in these params if a new base transformer / InstantX ControlNet pair get released. + self._flux_transformer_num_double_blocks = 19 + self._flux_transformer_num_single_blocks = 38 + + @classmethod + def prepare_controlnet_cond( + cls, + controlnet_image: Image, + vae_info: LoadedModel, + latent_height: int, + latent_width: int, + dtype: torch.dtype, + device: torch.device, + resize_mode: CONTROLNET_RESIZE_VALUES, + ): + image_height = latent_height * LATENT_SCALE_FACTOR + image_width = latent_width * LATENT_SCALE_FACTOR + + resized_controlnet_image = prepare_control_image( + image=controlnet_image, + do_classifier_free_guidance=False, + width=image_width, + height=image_height, + device=device, + dtype=dtype, + control_mode="balanced", + resize_mode=resize_mode, + ) + + # Shift the image from [0, 1] to [-1, 1]. + resized_controlnet_image = resized_controlnet_image * 2 - 1 + + # Run VAE encoder. + controlnet_cond = FluxVaeEncodeInvocation.vae_encode(vae_info=vae_info, image_tensor=resized_controlnet_image) + controlnet_cond = pack(controlnet_cond) + + return controlnet_cond + + @classmethod + def from_controlnet_image( + cls, + model: InstantXControlNetFlux, + controlnet_image: Image, + instantx_control_mode: torch.Tensor | None, + vae_info: LoadedModel, + latent_height: int, + latent_width: int, + dtype: torch.dtype, + device: torch.device, + resize_mode: CONTROLNET_RESIZE_VALUES, + weight: Union[float, List[float]], + begin_step_percent: float, + end_step_percent: float, + ): + image_height = latent_height * LATENT_SCALE_FACTOR + image_width = latent_width * LATENT_SCALE_FACTOR + + resized_controlnet_image = prepare_control_image( + image=controlnet_image, + do_classifier_free_guidance=False, + width=image_width, + height=image_height, + device=device, + dtype=dtype, + control_mode="balanced", + resize_mode=resize_mode, + ) + + # Shift the image from [0, 1] to [-1, 1]. + resized_controlnet_image = resized_controlnet_image * 2 - 1 + + # Run VAE encoder. + controlnet_cond = FluxVaeEncodeInvocation.vae_encode(vae_info=vae_info, image_tensor=resized_controlnet_image) + controlnet_cond = pack(controlnet_cond) + + return cls( + model=model, + controlnet_cond=controlnet_cond, + instantx_control_mode=instantx_control_mode, + weight=weight, + begin_step_percent=begin_step_percent, + end_step_percent=end_step_percent, + ) + + def _instantx_output_to_controlnet_output( + self, instantx_output: InstantXControlNetFluxOutput + ) -> ControlNetFluxOutput: + # The `interval_control` logic here is based on + # https://github.com/huggingface/diffusers/blob/31058cdaef63ca660a1a045281d156239fba8192/src/diffusers/models/transformers/transformer_flux.py#L507-L511 + + # Handle double block residuals. + double_block_residuals: list[torch.Tensor] = [] + double_block_samples = instantx_output.controlnet_block_samples + if double_block_samples: + interval_control = self._flux_transformer_num_double_blocks / len(double_block_samples) + interval_control = int(math.ceil(interval_control)) + for i in range(self._flux_transformer_num_double_blocks): + double_block_residuals.append(double_block_samples[i // interval_control]) + + # Handle single block residuals. + single_block_residuals: list[torch.Tensor] = [] + single_block_samples = instantx_output.controlnet_single_block_samples + if single_block_samples: + interval_control = self._flux_transformer_num_single_blocks / len(single_block_samples) + interval_control = int(math.ceil(interval_control)) + for i in range(self._flux_transformer_num_single_blocks): + single_block_residuals.append(single_block_samples[i // interval_control]) + + return ControlNetFluxOutput( + double_block_residuals=double_block_residuals or None, + single_block_residuals=single_block_residuals or None, + ) + + def run_controlnet( + self, + timestep_index: int, + total_num_timesteps: int, + img: torch.Tensor, + img_ids: torch.Tensor, + txt: torch.Tensor, + txt_ids: torch.Tensor, + y: torch.Tensor, + timesteps: torch.Tensor, + guidance: torch.Tensor | None, + ) -> ControlNetFluxOutput: + weight = self._get_weight(timestep_index=timestep_index, total_num_timesteps=total_num_timesteps) + if weight < 1e-6: + return ControlNetFluxOutput(single_block_residuals=None, double_block_residuals=None) + + # Make sure inputs have correct device and dtype. + self._controlnet_cond = self._controlnet_cond.to(device=img.device, dtype=img.dtype) + self._instantx_control_mode = ( + self._instantx_control_mode.to(device=img.device) if self._instantx_control_mode is not None else None + ) + + instantx_output: InstantXControlNetFluxOutput = self._model( + controlnet_cond=self._controlnet_cond, + controlnet_mode=self._instantx_control_mode, + img=img, + img_ids=img_ids, + txt=txt, + txt_ids=txt_ids, + timesteps=timesteps, + y=y, + guidance=guidance, + ) + + controlnet_output = self._instantx_output_to_controlnet_output(instantx_output) + controlnet_output.apply_weight(weight) + return controlnet_output diff --git a/invokeai/backend/flux/extensions/kontext_extension.py b/invokeai/backend/flux/extensions/kontext_extension.py new file mode 100644 index 00000000000..b58c670115b --- /dev/null +++ b/invokeai/backend/flux/extensions/kontext_extension.py @@ -0,0 +1,218 @@ +import torch +import torch.nn.functional as F +import torchvision.transforms as T +from einops import repeat + +from invokeai.app.invocations.fields import FluxKontextConditioningField +from invokeai.app.invocations.model import VAEField +from invokeai.app.services.shared.invocation_context import InvocationContext +from invokeai.backend.flux.modules.autoencoder import AutoEncoder +from invokeai.backend.flux.sampling_utils import pack +from invokeai.backend.util.devices import TorchDevice + + +def generate_img_ids_with_offset( + latent_height: int, + latent_width: int, + batch_size: int, + device: torch.device, + dtype: torch.dtype, + idx_offset: int = 0, + h_offset: int = 0, + w_offset: int = 0, +) -> torch.Tensor: + """Generate tensor of image position ids with optional index and spatial offsets. + + Args: + latent_height (int): Height of image in latent space (after packing, this becomes h//2). + latent_width (int): Width of image in latent space (after packing, this becomes w//2). + batch_size (int): Number of images in the batch. + device (torch.device): Device to create tensors on. + dtype (torch.dtype): Data type for the tensors. + idx_offset (int): Offset to add to the first dimension of the image ids (default: 0). + h_offset (int): Spatial offset for height/y-coordinates in latent space (default: 0). + w_offset (int): Spatial offset for width/x-coordinates in latent space (default: 0). + + Returns: + torch.Tensor: Image position ids with shape [batch_size, (latent_height//2 * latent_width//2), 3]. + """ + + if device.type == "mps": + orig_dtype = dtype + dtype = torch.float16 + + # After packing, the spatial dimensions are halved due to the 2x2 patch structure + packed_height = latent_height // 2 + packed_width = latent_width // 2 + + # Convert spatial offsets from latent space to packed space + packed_h_offset = h_offset // 2 + packed_w_offset = w_offset // 2 + + # Create base tensor for position IDs with shape [packed_height, packed_width, 3] + # The 3 channels represent: [batch_offset, y_position, x_position] + img_ids = torch.zeros(packed_height, packed_width, 3, device=device, dtype=dtype) + + # Set the batch offset for all positions + img_ids[..., 0] = idx_offset + + # Create y-coordinate indices (vertical positions) with spatial offset + y_indices = torch.arange(packed_height, device=device, dtype=dtype) + packed_h_offset + # Broadcast y_indices to match the spatial dimensions [packed_height, 1] + img_ids[..., 1] = y_indices[:, None] + + # Create x-coordinate indices (horizontal positions) with spatial offset + x_indices = torch.arange(packed_width, device=device, dtype=dtype) + packed_w_offset + # Broadcast x_indices to match the spatial dimensions [1, packed_width] + img_ids[..., 2] = x_indices[None, :] + + # Expand to include batch dimension: [batch_size, (packed_height * packed_width), 3] + img_ids = repeat(img_ids, "h w c -> b (h w) c", b=batch_size) + + if device.type == "mps": + img_ids = img_ids.to(orig_dtype) + + return img_ids + + +class KontextExtension: + """Applies FLUX Kontext (reference image) conditioning.""" + + def __init__( + self, + kontext_conditioning: list[FluxKontextConditioningField], + context: InvocationContext, + vae_field: VAEField, + device: torch.device, + dtype: torch.dtype, + ): + """ + Initializes the KontextExtension, pre-processing the reference images + into latents and positional IDs. + """ + self._context = context + self._device = device + self._dtype = dtype + self._vae_field = vae_field + self.kontext_conditioning = kontext_conditioning + + # Pre-process and cache the kontext latents and ids upon initialization. + self.kontext_latents, self.kontext_ids = self._prepare_kontext() + + def _prepare_kontext(self) -> tuple[torch.Tensor, torch.Tensor]: + """Encodes the reference images and prepares their concatenated latents and IDs with spatial tiling.""" + all_latents = [] + all_ids = [] + + # Track cumulative dimensions for spatial tiling + # These track the running extent of the virtual canvas in latent space + canvas_h = 0 # Running canvas height + canvas_w = 0 # Running canvas width + + vae_info = self._context.models.load(self._vae_field.vae) + + for idx, kontext_field in enumerate(self.kontext_conditioning): + image = self._context.images.get_pil(kontext_field.image.image_name) + + # Convert to RGB + image = image.convert("RGB") + + # Convert to tensor using torchvision transforms for consistency + transformation = T.Compose( + [ + T.ToTensor(), # Converts PIL image to tensor and scales to [0, 1] + ] + ) + image_tensor = transformation(image) + # Convert from [0, 1] to [-1, 1] range expected by VAE + image_tensor = image_tensor * 2.0 - 1.0 + image_tensor = image_tensor.unsqueeze(0) # Add batch dimension + image_tensor = image_tensor.to(self._device) + + # Continue with VAE encoding + # Don't sample from the distribution for reference images - use the mean (matching ComfyUI) + # Estimate working memory for encode operation (50% of decode memory requirements) + img_h = image_tensor.shape[-2] + img_w = image_tensor.shape[-1] + element_size = next(vae_info.model.parameters()).element_size() + scaling_constant = 1100 # 50% of decode scaling constant (2200) + estimated_working_memory = int(img_h * img_w * element_size * scaling_constant) + + 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) + # Use sample=False to get the distribution mean without noise + kontext_latents_unpacked = vae.encode(image_tensor, sample=False) + TorchDevice.empty_cache() + + # Extract tensor dimensions + batch_size, _, latent_height, latent_width = kontext_latents_unpacked.shape + + # Pad latents to be compatible with patch_size=2 + # This ensures dimensions are even for the pack() function + pad_h = (2 - latent_height % 2) % 2 + pad_w = (2 - latent_width % 2) % 2 + if pad_h > 0 or pad_w > 0: + kontext_latents_unpacked = F.pad(kontext_latents_unpacked, (0, pad_w, 0, pad_h), mode="circular") + # Update dimensions after padding + _, _, latent_height, latent_width = kontext_latents_unpacked.shape + + # Pack the latents + kontext_latents_packed = pack(kontext_latents_unpacked).to(self._device, self._dtype) + + # Determine spatial offsets for this reference image + h_offset = 0 + w_offset = 0 + + if idx > 0: # First image starts at (0, 0) + # Calculate potential canvas dimensions for each tiling option + # Option 1: Tile vertically (below existing content) + potential_h_vertical = canvas_h + latent_height + + # Option 2: Tile horizontally (to the right of existing content) + potential_w_horizontal = canvas_w + latent_width + + # Choose arrangement that minimizes the maximum dimension + # This keeps the canvas closer to square, optimizing attention computation + if potential_h_vertical > potential_w_horizontal: + # Tile horizontally (to the right of existing images) + w_offset = canvas_w + canvas_w = canvas_w + latent_width + canvas_h = max(canvas_h, latent_height) + else: + # Tile vertically (below existing images) + h_offset = canvas_h + canvas_h = canvas_h + latent_height + canvas_w = max(canvas_w, latent_width) + else: + # First image - just set canvas dimensions + canvas_h = latent_height + canvas_w = latent_width + + # Generate IDs with both index offset and spatial offsets + kontext_ids = generate_img_ids_with_offset( + latent_height=latent_height, + latent_width=latent_width, + batch_size=batch_size, + device=self._device, + dtype=self._dtype, + idx_offset=1, # All reference images use index=1 (matching ComfyUI implementation) + h_offset=h_offset, + w_offset=w_offset, + ) + + all_latents.append(kontext_latents_packed) + all_ids.append(kontext_ids) + + # Concatenate all latents and IDs along the sequence dimension + concatenated_latents = torch.cat(all_latents, dim=1) # Concatenate along sequence dimension + concatenated_ids = torch.cat(all_ids, dim=1) # Concatenate along sequence dimension + + return concatenated_latents, concatenated_ids + + def ensure_batch_size(self, target_batch_size: int) -> None: + """Ensures the kontext latents and IDs match the target batch size by repeating if necessary.""" + if self.kontext_latents.shape[0] != target_batch_size: + self.kontext_latents = self.kontext_latents.repeat(target_batch_size, 1, 1) + self.kontext_ids = self.kontext_ids.repeat(target_batch_size, 1, 1) diff --git a/invokeai/backend/flux/extensions/regional_prompting_extension.py b/invokeai/backend/flux/extensions/regional_prompting_extension.py new file mode 100644 index 00000000000..b5f42a44036 --- /dev/null +++ b/invokeai/backend/flux/extensions/regional_prompting_extension.py @@ -0,0 +1,295 @@ +from typing import Optional + +import torch +import torchvision + +from invokeai.backend.flux.text_conditioning import ( + FluxReduxConditioning, + FluxRegionalTextConditioning, + FluxTextConditioning, +) +from invokeai.backend.stable_diffusion.diffusion.conditioning_data import Range +from invokeai.backend.util.devices import TorchDevice +from invokeai.backend.util.mask import to_standard_float_mask + + +class RegionalPromptingExtension: + """A class for managing regional prompting with FLUX. + + This implementation is inspired by https://arxiv.org/pdf/2411.02395 (though there are significant differences). + """ + + def __init__( + self, + regional_text_conditioning: FluxRegionalTextConditioning, + restricted_attn_mask: torch.Tensor | None = None, + ): + self.regional_text_conditioning = regional_text_conditioning + self.restricted_attn_mask = restricted_attn_mask + + def get_double_stream_attn_mask(self, block_index: int) -> torch.Tensor | None: + order = [self.restricted_attn_mask, None] + return order[block_index % len(order)] + + def get_single_stream_attn_mask(self, block_index: int) -> torch.Tensor | None: + order = [self.restricted_attn_mask, None] + return order[block_index % len(order)] + + @classmethod + def from_text_conditioning( + cls, + text_conditioning: list[FluxTextConditioning], + redux_conditioning: list[FluxReduxConditioning], + img_seq_len: int, + ): + """Create a RegionalPromptingExtension from a list of text conditionings. + + Args: + text_conditioning (list[FluxTextConditioning]): The text conditionings to use for regional prompting. + img_seq_len (int): The image sequence length (i.e. packed_height * packed_width). + """ + regional_text_conditioning = cls._concat_regional_text_conditioning(text_conditioning, redux_conditioning) + attn_mask_with_restricted_img_self_attn = cls._prepare_restricted_attn_mask( + regional_text_conditioning, img_seq_len + ) + return cls( + regional_text_conditioning=regional_text_conditioning, + restricted_attn_mask=attn_mask_with_restricted_img_self_attn, + ) + + # Keeping _prepare_unrestricted_attn_mask for reference as an alternative masking strategy: + # + # @classmethod + # def _prepare_unrestricted_attn_mask( + # cls, + # regional_text_conditioning: FluxRegionalTextConditioning, + # img_seq_len: int, + # ) -> torch.Tensor: + # """Prepare an 'unrestricted' attention mask. In this context, 'unrestricted' means that: + # - img self-attention is not masked. + # - img regions attend to both txt within their own region and to global prompts. + # """ + # device = TorchDevice.choose_torch_device() + + # # Infer txt_seq_len from the t5_embeddings tensor. + # txt_seq_len = regional_text_conditioning.t5_embeddings.shape[1] + + # # In the attention blocks, the txt seq and img seq are concatenated and then attention is applied. + # # Concatenation happens in the following order: [txt_seq, img_seq]. + # # There are 4 portions of the attention mask to consider as we prepare it: + # # 1. txt attends to itself + # # 2. txt attends to corresponding regional img + # # 3. regional img attends to corresponding txt + # # 4. regional img attends to itself + + # # Initialize empty attention mask. + # regional_attention_mask = torch.zeros( + # (txt_seq_len + img_seq_len, txt_seq_len + img_seq_len), device=device, dtype=torch.float16 + # ) + + # for image_mask, t5_embedding_range in zip( + # regional_text_conditioning.image_masks, regional_text_conditioning.t5_embedding_ranges, strict=True + # ): + # # 1. txt attends to itself + # regional_attention_mask[ + # t5_embedding_range.start : t5_embedding_range.end, t5_embedding_range.start : t5_embedding_range.end + # ] = 1.0 + + # # 2. txt attends to corresponding regional img + # # Note that we reshape to (1, img_seq_len) to ensure broadcasting works as desired. + # fill_value = image_mask.view(1, img_seq_len) if image_mask is not None else 1.0 + # regional_attention_mask[t5_embedding_range.start : t5_embedding_range.end, txt_seq_len:] = fill_value + + # # 3. regional img attends to corresponding txt + # # Note that we reshape to (img_seq_len, 1) to ensure broadcasting works as desired. + # fill_value = image_mask.view(img_seq_len, 1) if image_mask is not None else 1.0 + # regional_attention_mask[txt_seq_len:, t5_embedding_range.start : t5_embedding_range.end] = fill_value + + # # 4. regional img attends to itself + # # Allow unrestricted img self attention. + # regional_attention_mask[txt_seq_len:, txt_seq_len:] = 1.0 + + # # Convert attention mask to boolean. + # regional_attention_mask = regional_attention_mask > 0.5 + + # return regional_attention_mask + + @classmethod + def _prepare_restricted_attn_mask( + cls, + regional_text_conditioning: FluxRegionalTextConditioning, + img_seq_len: int, + ) -> torch.Tensor | None: + """Prepare a 'restricted' attention mask. In this context, 'restricted' means that: + - img self-attention is only allowed within regions. + - img regions only attend to txt within their own region, not to global prompts. + """ + # Identify background region. I.e. the region that is not covered by any region masks. + background_region_mask: None | torch.Tensor = None + for image_mask in regional_text_conditioning.image_masks: + if image_mask is not None: + if background_region_mask is None: + background_region_mask = torch.ones_like(image_mask) + background_region_mask *= 1 - image_mask + + if background_region_mask is None: + # There are no region masks, short-circuit and return None. + # TODO(ryand): We could restrict txt-txt attention across multiple global prompts, but this would + # is a rare use case and would make the logic here significantly more complicated. + return None + + device = TorchDevice.choose_torch_device() + + # Infer txt_seq_len from the t5_embeddings tensor. + txt_seq_len = regional_text_conditioning.t5_embeddings.shape[1] + + # In the attention blocks, the txt seq and img seq are concatenated and then attention is applied. + # Concatenation happens in the following order: [txt_seq, img_seq]. + # There are 4 portions of the attention mask to consider as we prepare it: + # 1. txt attends to itself + # 2. txt attends to corresponding regional img + # 3. regional img attends to corresponding txt + # 4. regional img attends to itself + + # Initialize empty attention mask. + regional_attention_mask = torch.zeros( + (txt_seq_len + img_seq_len, txt_seq_len + img_seq_len), device=device, dtype=torch.float16 + ) + + for image_mask, t5_embedding_range in zip( + regional_text_conditioning.image_masks, regional_text_conditioning.t5_embedding_ranges, strict=True + ): + # 1. txt attends to itself + regional_attention_mask[ + t5_embedding_range.start : t5_embedding_range.end, t5_embedding_range.start : t5_embedding_range.end + ] = 1.0 + + if image_mask is not None: + # 2. txt attends to corresponding regional img + # Note that we reshape to (1, img_seq_len) to ensure broadcasting works as desired. + regional_attention_mask[t5_embedding_range.start : t5_embedding_range.end, txt_seq_len:] = ( + image_mask.view(1, img_seq_len) + ) + + # 3. regional img attends to corresponding txt + # Note that we reshape to (img_seq_len, 1) to ensure broadcasting works as desired. + regional_attention_mask[txt_seq_len:, t5_embedding_range.start : t5_embedding_range.end] = ( + image_mask.view(img_seq_len, 1) + ) + + # 4. regional img attends to itself + image_mask = image_mask.view(img_seq_len, 1) + regional_attention_mask[txt_seq_len:, txt_seq_len:] += image_mask @ image_mask.T + else: + # We don't allow attention between non-background image regions and global prompts. This helps to ensure + # that regions focus on their local prompts. We do, however, allow attention between background regions + # and global prompts. If we didn't do this, then the background regions would not attend to any txt + # embeddings, which we found experimentally to cause artifacts. + + # 2. global txt attends to background region + # Note that we reshape to (1, img_seq_len) to ensure broadcasting works as desired. + regional_attention_mask[t5_embedding_range.start : t5_embedding_range.end, txt_seq_len:] = ( + background_region_mask.view(1, img_seq_len) + ) + + # 3. background region attends to global txt + # Note that we reshape to (img_seq_len, 1) to ensure broadcasting works as desired. + regional_attention_mask[txt_seq_len:, t5_embedding_range.start : t5_embedding_range.end] = ( + background_region_mask.view(img_seq_len, 1) + ) + + # Allow background regions to attend to themselves. + regional_attention_mask[txt_seq_len:, txt_seq_len:] += background_region_mask.view(img_seq_len, 1) + regional_attention_mask[txt_seq_len:, txt_seq_len:] += background_region_mask.view(1, img_seq_len) + + # Convert attention mask to boolean. + regional_attention_mask = regional_attention_mask > 0.5 + + return regional_attention_mask + + @classmethod + def _concat_regional_text_conditioning( + cls, + text_conditionings: list[FluxTextConditioning], + redux_conditionings: list[FluxReduxConditioning], + ) -> FluxRegionalTextConditioning: + """Concatenate regional text conditioning data into a single conditioning tensor (with associated masks).""" + concat_t5_embeddings: list[torch.Tensor] = [] + concat_t5_embedding_ranges: list[Range] = [] + image_masks: list[torch.Tensor | None] = [] + + # Choose global CLIP embedding. + # Use the first global prompt's CLIP embedding as the global CLIP embedding. If there is no global prompt, use + # the first prompt's CLIP embedding. + global_clip_embedding: torch.Tensor = text_conditionings[0].clip_embeddings + for text_conditioning in text_conditionings: + if text_conditioning.mask is None: + global_clip_embedding = text_conditioning.clip_embeddings + break + + # Handle T5 text embeddings. + cur_t5_embedding_len = 0 + for text_conditioning in text_conditionings: + concat_t5_embeddings.append(text_conditioning.t5_embeddings) + concat_t5_embedding_ranges.append( + Range(start=cur_t5_embedding_len, end=cur_t5_embedding_len + text_conditioning.t5_embeddings.shape[1]) + ) + image_masks.append(text_conditioning.mask) + cur_t5_embedding_len += text_conditioning.t5_embeddings.shape[1] + + # Handle Redux embeddings. + for redux_conditioning in redux_conditionings: + concat_t5_embeddings.append(redux_conditioning.redux_embeddings) + concat_t5_embedding_ranges.append( + Range( + start=cur_t5_embedding_len, end=cur_t5_embedding_len + redux_conditioning.redux_embeddings.shape[1] + ) + ) + image_masks.append(redux_conditioning.mask) + cur_t5_embedding_len += redux_conditioning.redux_embeddings.shape[1] + + t5_embeddings = torch.cat(concat_t5_embeddings, dim=1) + + # Initialize the txt_ids tensor. + pos_bs, pos_t5_seq_len, _ = t5_embeddings.shape + t5_txt_ids = torch.zeros( + pos_bs, pos_t5_seq_len, 3, dtype=t5_embeddings.dtype, device=TorchDevice.choose_torch_device() + ) + + return FluxRegionalTextConditioning( + t5_embeddings=t5_embeddings, + clip_embeddings=global_clip_embedding, + t5_txt_ids=t5_txt_ids, + image_masks=image_masks, + t5_embedding_ranges=concat_t5_embedding_ranges, + ) + + @staticmethod + def preprocess_regional_prompt_mask( + mask: Optional[torch.Tensor], packed_height: int, packed_width: int, dtype: torch.dtype, device: torch.device + ) -> 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. + + packed_height and packed_width are the target height and width of the mask in the 'packed' latent space. + + Returns: + torch.Tensor: The processed mask. shape: (1, 1, packed_height * packed_width). + """ + + if mask is None: + return torch.ones((1, 1, packed_height * packed_width), dtype=dtype, device=device) + + mask = to_standard_float_mask(mask, out_dtype=dtype) + + tf = torchvision.transforms.Resize( + (packed_height, packed_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) + + # Flatten the height and width dimensions into a single image_seq_len dimension. + return resized_mask.flatten(start_dim=2) diff --git a/invokeai/backend/flux/extensions/xlabs_controlnet_extension.py b/invokeai/backend/flux/extensions/xlabs_controlnet_extension.py new file mode 100644 index 00000000000..1f6409cbbe5 --- /dev/null +++ b/invokeai/backend/flux/extensions/xlabs_controlnet_extension.py @@ -0,0 +1,150 @@ +from typing import List, Union + +import torch +from PIL.Image import Image + +from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR +from invokeai.app.util.controlnet_utils import CONTROLNET_RESIZE_VALUES, prepare_control_image +from invokeai.backend.flux.controlnet.controlnet_flux_output import ControlNetFluxOutput +from invokeai.backend.flux.controlnet.xlabs_controlnet_flux import XLabsControlNetFlux, XLabsControlNetFluxOutput +from invokeai.backend.flux.extensions.base_controlnet_extension import BaseControlNetExtension + + +class XLabsControlNetExtension(BaseControlNetExtension): + def __init__( + self, + model: XLabsControlNetFlux, + controlnet_cond: torch.Tensor, + weight: Union[float, List[float]], + begin_step_percent: float, + end_step_percent: float, + ): + super().__init__( + weight=weight, + begin_step_percent=begin_step_percent, + end_step_percent=end_step_percent, + ) + + self._model = model + # _controlnet_cond is the control image passed to the ControlNet model. + # Pixel values are in the range [-1, 1]. Shape: (batch_size, 3, height, width). + self._controlnet_cond = controlnet_cond + + # TODO(ryand): Pass in these params if a new base transformer / XLabs ControlNet pair get released. + self._flux_transformer_num_double_blocks = 19 + self._flux_transformer_num_single_blocks = 38 + + @classmethod + def prepare_controlnet_cond( + cls, + controlnet_image: Image, + latent_height: int, + latent_width: int, + dtype: torch.dtype, + device: torch.device, + resize_mode: CONTROLNET_RESIZE_VALUES, + ): + image_height = latent_height * LATENT_SCALE_FACTOR + image_width = latent_width * LATENT_SCALE_FACTOR + + controlnet_cond = prepare_control_image( + image=controlnet_image, + do_classifier_free_guidance=False, + width=image_width, + height=image_height, + device=device, + dtype=dtype, + control_mode="balanced", + resize_mode=resize_mode, + ) + + # Map pixel values from [0, 1] to [-1, 1]. + controlnet_cond = controlnet_cond * 2 - 1 + + return controlnet_cond + + @classmethod + def from_controlnet_image( + cls, + model: XLabsControlNetFlux, + controlnet_image: Image, + latent_height: int, + latent_width: int, + dtype: torch.dtype, + device: torch.device, + resize_mode: CONTROLNET_RESIZE_VALUES, + weight: Union[float, List[float]], + begin_step_percent: float, + end_step_percent: float, + ): + image_height = latent_height * LATENT_SCALE_FACTOR + image_width = latent_width * LATENT_SCALE_FACTOR + + controlnet_cond = prepare_control_image( + image=controlnet_image, + do_classifier_free_guidance=False, + width=image_width, + height=image_height, + device=device, + dtype=dtype, + control_mode="balanced", + resize_mode=resize_mode, + ) + + # Map pixel values from [0, 1] to [-1, 1]. + controlnet_cond = controlnet_cond * 2 - 1 + + return cls( + model=model, + controlnet_cond=controlnet_cond, + weight=weight, + begin_step_percent=begin_step_percent, + end_step_percent=end_step_percent, + ) + + def _xlabs_output_to_controlnet_output(self, xlabs_output: XLabsControlNetFluxOutput) -> ControlNetFluxOutput: + # The modulo index logic used here is based on: + # https://github.com/XLabs-AI/x-flux/blob/47495425dbed499be1e8e5a6e52628b07349cba2/src/flux/model.py#L198-L200 + + # Handle double block residuals. + double_block_residuals: list[torch.Tensor] = [] + xlabs_double_block_residuals = xlabs_output.controlnet_double_block_residuals + if xlabs_double_block_residuals is not None: + for i in range(self._flux_transformer_num_double_blocks): + double_block_residuals.append(xlabs_double_block_residuals[i % len(xlabs_double_block_residuals)]) + + return ControlNetFluxOutput( + double_block_residuals=double_block_residuals, + single_block_residuals=None, + ) + + def run_controlnet( + self, + timestep_index: int, + total_num_timesteps: int, + img: torch.Tensor, + img_ids: torch.Tensor, + txt: torch.Tensor, + txt_ids: torch.Tensor, + y: torch.Tensor, + timesteps: torch.Tensor, + guidance: torch.Tensor | None, + ) -> ControlNetFluxOutput: + weight = self._get_weight(timestep_index=timestep_index, total_num_timesteps=total_num_timesteps) + if weight < 1e-6: + return ControlNetFluxOutput(single_block_residuals=None, double_block_residuals=None) + + xlabs_output: XLabsControlNetFluxOutput = self._model( + img=img, + img_ids=img_ids, + controlnet_cond=self._controlnet_cond, + txt=txt, + txt_ids=txt_ids, + timesteps=timesteps, + y=y, + guidance=guidance, + ) + + controlnet_output = self._xlabs_output_to_controlnet_output(xlabs_output) + controlnet_output.apply_weight(weight) + return controlnet_output diff --git a/invokeai/backend/flux/extensions/xlabs_ip_adapter_extension.py b/invokeai/backend/flux/extensions/xlabs_ip_adapter_extension.py new file mode 100644 index 00000000000..3cae4707e67 --- /dev/null +++ b/invokeai/backend/flux/extensions/xlabs_ip_adapter_extension.py @@ -0,0 +1,90 @@ +import math +from typing import List, Union + +import einops +import torch +from PIL import Image +from transformers import CLIPImageProcessor, CLIPVisionModelWithProjection + +from invokeai.backend.flux.ip_adapter.xlabs_ip_adapter_flux import XlabsIpAdapterFlux +from invokeai.backend.flux.modules.layers import DoubleStreamBlock +from invokeai.backend.util.devices import TorchDevice + + +class XLabsIPAdapterExtension: + def __init__( + self, + model: XlabsIpAdapterFlux, + image_prompt_clip_embed: torch.Tensor, + weight: Union[float, List[float]], + begin_step_percent: float, + end_step_percent: float, + ): + self._model = model + self._image_prompt_clip_embed = image_prompt_clip_embed + self._weight = weight + self._begin_step_percent = begin_step_percent + self._end_step_percent = end_step_percent + + self._image_proj: torch.Tensor | None = None + + def _get_weight(self, timestep_index: int, total_num_timesteps: int) -> float: + first_step = math.floor(self._begin_step_percent * total_num_timesteps) + last_step = math.ceil(self._end_step_percent * total_num_timesteps) + + if timestep_index < first_step or timestep_index > last_step: + return 0.0 + + if isinstance(self._weight, list): + return self._weight[timestep_index] + + return self._weight + + @staticmethod + def run_clip_image_encoder( + pil_image: List[Image.Image], image_encoder: CLIPVisionModelWithProjection + ) -> torch.Tensor: + clip_image_processor = CLIPImageProcessor() + clip_image: torch.Tensor = clip_image_processor(images=pil_image, return_tensors="pt").pixel_values + clip_image = clip_image.to(device=TorchDevice.choose_torch_device(), dtype=image_encoder.dtype) + clip_image_embeds = image_encoder(clip_image).image_embeds + return clip_image_embeds + + def run_image_proj(self, dtype: torch.dtype): + image_prompt_clip_embed = self._image_prompt_clip_embed.to(dtype=dtype) + self._image_proj = self._model.image_proj(image_prompt_clip_embed) + + def run_ip_adapter( + self, + timestep_index: int, + total_num_timesteps: int, + block_index: int, + block: DoubleStreamBlock, + img_q: torch.Tensor, + img: torch.Tensor, + ) -> torch.Tensor: + """The logic in this function is based on: + https://github.com/XLabs-AI/x-flux/blob/47495425dbed499be1e8e5a6e52628b07349cba2/src/flux/modules/layers.py#L245-L301 + """ + weight = self._get_weight(timestep_index=timestep_index, total_num_timesteps=total_num_timesteps) + if weight < 1e-6: + return img + + ip_adapter_block = self._model.ip_adapter_double_blocks.double_blocks[block_index] + + ip_key = ip_adapter_block.ip_adapter_double_stream_k_proj(self._image_proj) + ip_value = ip_adapter_block.ip_adapter_double_stream_v_proj(self._image_proj) + + # Reshape projections for multi-head attention. + ip_key = einops.rearrange(ip_key, "B L (H D) -> B H L D", H=block.num_heads) + ip_value = einops.rearrange(ip_value, "B L (H D) -> B H L D", H=block.num_heads) + + # Compute attention between IP projections and the latent query. + ip_attn = torch.nn.functional.scaled_dot_product_attention( + img_q, ip_key, ip_value, dropout_p=0.0, is_causal=False + ) + ip_attn = einops.rearrange(ip_attn, "B H L D -> B L (H D)", H=block.num_heads) + + img = img + weight * ip_attn + + return img diff --git a/invokeai/backend/flux/flux_state_dict_utils.py b/invokeai/backend/flux/flux_state_dict_utils.py new file mode 100644 index 00000000000..c306c88f965 --- /dev/null +++ b/invokeai/backend/flux/flux_state_dict_utils.py @@ -0,0 +1,20 @@ +from typing import Any + + +def get_flux_in_channels_from_state_dict(state_dict: dict[str | int, Any]) -> int | None: + """Gets the in channels from the state dict.""" + + # "Standard" FLUX models use "img_in.weight", but some community fine tunes use + # "model.diffusion_model.img_in.weight". Known models that use the latter key: + # - https://civitai.com/models/885098?modelVersionId=990775 + # - https://civitai.com/models/1018060?modelVersionId=1596255 + # - https://civitai.com/models/978314/ultrareal-fine-tune?modelVersionId=1413133 + + keys = {"img_in.weight", "model.diffusion_model.img_in.weight"} + + for key in keys: + val = state_dict.get(key) + if val is not None: + return val.shape[1] + + return None diff --git a/invokeai/backend/flux/ip_adapter/__init__.py b/invokeai/backend/flux/ip_adapter/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/flux/ip_adapter/ip_double_stream_block_processor.py b/invokeai/backend/flux/ip_adapter/ip_double_stream_block_processor.py new file mode 100644 index 00000000000..9b1bef7f707 --- /dev/null +++ b/invokeai/backend/flux/ip_adapter/ip_double_stream_block_processor.py @@ -0,0 +1,93 @@ +# This file is based on: +# https://github.com/XLabs-AI/x-flux/blob/47495425dbed499be1e8e5a6e52628b07349cba2/src/flux/modules/layers.py#L221 +import einops +import torch + +from invokeai.backend.flux.math import attention +from invokeai.backend.flux.modules.layers import DoubleStreamBlock + + +class IPDoubleStreamBlockProcessor(torch.nn.Module): + """Attention processor for handling IP-adapter with double stream block.""" + + def __init__(self, context_dim: int, hidden_dim: int): + super().__init__() + + # Ensure context_dim matches the dimension of image_proj + self.context_dim = context_dim + self.hidden_dim = hidden_dim + + # Initialize projections for IP-adapter + self.ip_adapter_double_stream_k_proj = torch.nn.Linear(context_dim, hidden_dim, bias=True) + self.ip_adapter_double_stream_v_proj = torch.nn.Linear(context_dim, hidden_dim, bias=True) + + torch.nn.init.zeros_(self.ip_adapter_double_stream_k_proj.weight) + torch.nn.init.zeros_(self.ip_adapter_double_stream_k_proj.bias) + + torch.nn.init.zeros_(self.ip_adapter_double_stream_v_proj.weight) + torch.nn.init.zeros_(self.ip_adapter_double_stream_v_proj.bias) + + def __call__( + self, + attn: DoubleStreamBlock, + img: torch.Tensor, + txt: torch.Tensor, + vec: torch.Tensor, + pe: torch.Tensor, + image_proj: torch.Tensor, + ip_scale: float = 1.0, + ): + # Prepare image for attention + img_mod1, img_mod2 = attn.img_mod(vec) + txt_mod1, txt_mod2 = attn.txt_mod(vec) + + img_modulated = attn.img_norm1(img) + img_modulated = (1 + img_mod1.scale) * img_modulated + img_mod1.shift + img_qkv = attn.img_attn.qkv(img_modulated) + img_q, img_k, img_v = einops.rearrange( + img_qkv, "B L (K H D) -> K B H L D", K=3, H=attn.num_heads, D=attn.head_dim + ) + img_q, img_k = attn.img_attn.norm(img_q, img_k, img_v) + + txt_modulated = attn.txt_norm1(txt) + txt_modulated = (1 + txt_mod1.scale) * txt_modulated + txt_mod1.shift + txt_qkv = attn.txt_attn.qkv(txt_modulated) + txt_q, txt_k, txt_v = einops.rearrange( + txt_qkv, "B L (K H D) -> K B H L D", K=3, H=attn.num_heads, D=attn.head_dim + ) + txt_q, txt_k = attn.txt_attn.norm(txt_q, txt_k, txt_v) + + q = torch.cat((txt_q, img_q), dim=2) + k = torch.cat((txt_k, img_k), dim=2) + v = torch.cat((txt_v, img_v), dim=2) + + attn1 = attention(q, k, v, pe=pe) + txt_attn, img_attn = attn1[:, : txt.shape[1]], attn1[:, txt.shape[1] :] + + # print(f"txt_attn shape: {txt_attn.size()}") + # print(f"img_attn shape: {img_attn.size()}") + + img = img + img_mod1.gate * attn.img_attn.proj(img_attn) + img = img + img_mod2.gate * attn.img_mlp((1 + img_mod2.scale) * attn.img_norm2(img) + img_mod2.shift) + + txt = txt + txt_mod1.gate * attn.txt_attn.proj(txt_attn) + txt = txt + txt_mod2.gate * attn.txt_mlp((1 + txt_mod2.scale) * attn.txt_norm2(txt) + txt_mod2.shift) + + # IP-adapter processing + ip_query = img_q # latent sample query + ip_key = self.ip_adapter_double_stream_k_proj(image_proj) + ip_value = self.ip_adapter_double_stream_v_proj(image_proj) + + # Reshape projections for multi-head attention + ip_key = einops.rearrange(ip_key, "B L (H D) -> B H L D", H=attn.num_heads, D=attn.head_dim) + ip_value = einops.rearrange(ip_value, "B L (H D) -> B H L D", H=attn.num_heads, D=attn.head_dim) + + # Compute attention between IP projections and the latent query + ip_attention = torch.nn.functional.scaled_dot_product_attention( + ip_query, ip_key, ip_value, dropout_p=0.0, is_causal=False + ) + ip_attention = einops.rearrange(ip_attention, "B H L D -> B L (H D)", H=attn.num_heads, D=attn.head_dim) + + img = img + ip_scale * ip_attention + + return img, txt diff --git a/invokeai/backend/flux/ip_adapter/state_dict_utils.py b/invokeai/backend/flux/ip_adapter/state_dict_utils.py new file mode 100644 index 00000000000..24ac53550f9 --- /dev/null +++ b/invokeai/backend/flux/ip_adapter/state_dict_utils.py @@ -0,0 +1,52 @@ +from typing import Any + +import torch + +from invokeai.backend.flux.ip_adapter.xlabs_ip_adapter_flux import XlabsIpAdapterParams + + +def is_state_dict_xlabs_ip_adapter(sd: dict[str | int, Any]) -> bool: + """Is the state dict for an XLabs FLUX IP-Adapter model? + + This is intended to be a reasonably high-precision detector, but it is not guaranteed to have perfect precision. + """ + # If all of the expected keys are present, then this is very likely an XLabs IP-Adapter model. + expected_keys = { + "double_blocks.0.processor.ip_adapter_double_stream_k_proj.bias", + "double_blocks.0.processor.ip_adapter_double_stream_k_proj.weight", + "double_blocks.0.processor.ip_adapter_double_stream_v_proj.bias", + "double_blocks.0.processor.ip_adapter_double_stream_v_proj.weight", + "ip_adapter_proj_model.norm.bias", + "ip_adapter_proj_model.norm.weight", + "ip_adapter_proj_model.proj.bias", + "ip_adapter_proj_model.proj.weight", + } + + if expected_keys.issubset(sd.keys()): + return True + return False + + +def infer_xlabs_ip_adapter_params_from_state_dict(state_dict: dict[str | int, torch.Tensor]) -> XlabsIpAdapterParams: + num_double_blocks = 0 + context_dim = 0 + hidden_dim = 0 + + # Count the number of double blocks. + double_block_index = 0 + while f"double_blocks.{double_block_index}.processor.ip_adapter_double_stream_k_proj.weight" in state_dict: + double_block_index += 1 + num_double_blocks = double_block_index + + hidden_dim = state_dict["double_blocks.0.processor.ip_adapter_double_stream_k_proj.weight"].shape[0] + context_dim = state_dict["double_blocks.0.processor.ip_adapter_double_stream_k_proj.weight"].shape[1] + clip_embeddings_dim = state_dict["ip_adapter_proj_model.proj.weight"].shape[1] + clip_extra_context_tokens = state_dict["ip_adapter_proj_model.proj.weight"].shape[0] // context_dim + + return XlabsIpAdapterParams( + num_double_blocks=num_double_blocks, + context_dim=context_dim, + hidden_dim=hidden_dim, + clip_embeddings_dim=clip_embeddings_dim, + clip_extra_context_tokens=clip_extra_context_tokens, + ) diff --git a/invokeai/backend/flux/ip_adapter/xlabs_ip_adapter_flux.py b/invokeai/backend/flux/ip_adapter/xlabs_ip_adapter_flux.py new file mode 100644 index 00000000000..0db05a69d89 --- /dev/null +++ b/invokeai/backend/flux/ip_adapter/xlabs_ip_adapter_flux.py @@ -0,0 +1,70 @@ +from dataclasses import dataclass + +import torch + +from invokeai.backend.ip_adapter.ip_adapter import ImageProjModel + + +class IPDoubleStreamBlock(torch.nn.Module): + def __init__(self, context_dim: int, hidden_dim: int): + super().__init__() + + self.context_dim = context_dim + self.hidden_dim = hidden_dim + + self.ip_adapter_double_stream_k_proj = torch.nn.Linear(context_dim, hidden_dim, bias=True) + self.ip_adapter_double_stream_v_proj = torch.nn.Linear(context_dim, hidden_dim, bias=True) + + +class IPAdapterDoubleBlocks(torch.nn.Module): + def __init__(self, num_double_blocks: int, context_dim: int, hidden_dim: int): + super().__init__() + self.double_blocks = torch.nn.ModuleList( + [IPDoubleStreamBlock(context_dim, hidden_dim) for _ in range(num_double_blocks)] + ) + + +@dataclass +class XlabsIpAdapterParams: + num_double_blocks: int + context_dim: int + hidden_dim: int + + clip_embeddings_dim: int + clip_extra_context_tokens: int + + +class XlabsIpAdapterFlux(torch.nn.Module): + def __init__(self, params: XlabsIpAdapterParams): + super().__init__() + self.image_proj = ImageProjModel( + cross_attention_dim=params.context_dim, + clip_embeddings_dim=params.clip_embeddings_dim, + clip_extra_context_tokens=params.clip_extra_context_tokens, + ) + self.ip_adapter_double_blocks = IPAdapterDoubleBlocks( + num_double_blocks=params.num_double_blocks, context_dim=params.context_dim, hidden_dim=params.hidden_dim + ) + + def load_xlabs_state_dict(self, state_dict: dict[str, torch.Tensor], assign: bool = False): + """We need this custom function to load state dicts rather than using .load_state_dict(...) because the model + structure does not match the state_dict structure. + """ + # Split the state_dict into the image projection model and the double blocks. + image_proj_sd: dict[str, torch.Tensor] = {} + double_blocks_sd: dict[str, torch.Tensor] = {} + for k, v in state_dict.items(): + if k.startswith("ip_adapter_proj_model."): + image_proj_sd[k] = v + elif k.startswith("double_blocks."): + double_blocks_sd[k] = v + else: + raise ValueError(f"Unexpected key: {k}") + + # Initialize the image projection model. + image_proj_sd = {k.replace("ip_adapter_proj_model.", ""): v for k, v in image_proj_sd.items()} + self.image_proj.load_state_dict(image_proj_sd, assign=assign) + + # Initialize the double blocks. + double_blocks_sd = {k.replace("processor.", ""): v for k, v in double_blocks_sd.items()} + self.ip_adapter_double_blocks.load_state_dict(double_blocks_sd, assign=assign) diff --git a/invokeai/backend/flux/math.py b/invokeai/backend/flux/math.py new file mode 100644 index 00000000000..57ff8259932 --- /dev/null +++ b/invokeai/backend/flux/math.py @@ -0,0 +1,35 @@ +# Initially pulled from https://github.com/black-forest-labs/flux + +import torch +from einops import rearrange +from torch import Tensor + + +def attention(q: Tensor, k: Tensor, v: Tensor, pe: Tensor, attn_mask: Tensor | None = None) -> Tensor: + q, k = apply_rope(q, k, pe) + + x = torch.nn.functional.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask) + x = rearrange(x, "B H L D -> B L (H D)") + + return x + + +def rope(pos: Tensor, dim: int, theta: int) -> Tensor: + assert dim % 2 == 0 + scale = ( + torch.arange(0, dim, 2, dtype=torch.float32 if pos.device.type == "mps" else torch.float64, device=pos.device) + / dim + ) + omega = 1.0 / (theta**scale) + out = torch.einsum("...n,d->...nd", pos, omega) + out = torch.stack([torch.cos(out), -torch.sin(out), torch.sin(out), torch.cos(out)], dim=-1) + out = rearrange(out, "b n d (i j) -> b n d i j", i=2, j=2) + return out.to(dtype=pos.dtype, device=pos.device) + + +def apply_rope(xq: Tensor, xk: Tensor, freqs_cis: Tensor) -> tuple[Tensor, Tensor]: + xq_ = xq.view(*xq.shape[:-1], -1, 1, 2) + xk_ = xk.view(*xk.shape[:-1], -1, 1, 2) + xq_out = freqs_cis[..., 0] * xq_[..., 0] + freqs_cis[..., 1] * xq_[..., 1] + xk_out = freqs_cis[..., 0] * xk_[..., 0] + freqs_cis[..., 1] * xk_[..., 1] + return xq_out.view(*xq.shape).type_as(xq), xk_out.view(*xk.shape).type_as(xk) diff --git a/invokeai/backend/flux/model.py b/invokeai/backend/flux/model.py new file mode 100644 index 00000000000..cfa85691e94 --- /dev/null +++ b/invokeai/backend/flux/model.py @@ -0,0 +1,168 @@ +# Initially pulled from https://github.com/black-forest-labs/flux + +from dataclasses import dataclass +from typing import Optional + +import torch +from torch import Tensor, nn + +from invokeai.backend.flux.custom_block_processor import ( + CustomDoubleStreamBlockProcessor, + CustomSingleStreamBlockProcessor, +) +from invokeai.backend.flux.extensions.regional_prompting_extension import RegionalPromptingExtension +from invokeai.backend.flux.extensions.xlabs_ip_adapter_extension import XLabsIPAdapterExtension +from invokeai.backend.flux.modules.layers import ( + DoubleStreamBlock, + EmbedND, + LastLayer, + MLPEmbedder, + SingleStreamBlock, + timestep_embedding, +) + + +@dataclass +class FluxParams: + in_channels: int + vec_in_dim: int + context_in_dim: int + hidden_size: int + mlp_ratio: float + num_heads: int + depth: int + depth_single_blocks: int + axes_dim: list[int] + theta: int + qkv_bias: bool + guidance_embed: bool + out_channels: Optional[int] = None + + +class Flux(nn.Module): + """ + Transformer model for flow matching on sequences. + """ + + def __init__(self, params: FluxParams): + super().__init__() + + self.params = params + self.in_channels = params.in_channels + self.out_channels = params.out_channels or self.in_channels + if params.hidden_size % params.num_heads != 0: + raise ValueError(f"Hidden size {params.hidden_size} must be divisible by num_heads {params.num_heads}") + pe_dim = params.hidden_size // params.num_heads + if sum(params.axes_dim) != pe_dim: + raise ValueError(f"Got {params.axes_dim} but expected positional dim {pe_dim}") + self.hidden_size = params.hidden_size + self.num_heads = params.num_heads + self.pe_embedder = EmbedND(dim=pe_dim, theta=params.theta, axes_dim=params.axes_dim) + self.img_in = nn.Linear(self.in_channels, self.hidden_size, bias=True) + self.time_in = MLPEmbedder(in_dim=256, hidden_dim=self.hidden_size) + self.vector_in = MLPEmbedder(params.vec_in_dim, self.hidden_size) + self.guidance_in = ( + MLPEmbedder(in_dim=256, hidden_dim=self.hidden_size) if params.guidance_embed else nn.Identity() + ) + self.txt_in = nn.Linear(params.context_in_dim, self.hidden_size) + + self.double_blocks = nn.ModuleList( + [ + DoubleStreamBlock( + self.hidden_size, + self.num_heads, + mlp_ratio=params.mlp_ratio, + qkv_bias=params.qkv_bias, + ) + for _ in range(params.depth) + ] + ) + + self.single_blocks = nn.ModuleList( + [ + SingleStreamBlock(self.hidden_size, self.num_heads, mlp_ratio=params.mlp_ratio) + for _ in range(params.depth_single_blocks) + ] + ) + + self.final_layer = LastLayer(self.hidden_size, 1, self.out_channels) + + def forward( + self, + img: Tensor, + img_ids: Tensor, + txt: Tensor, + txt_ids: Tensor, + timesteps: Tensor, + y: Tensor, + guidance: Tensor | None, + timestep_index: int, + total_num_timesteps: int, + controlnet_double_block_residuals: list[Tensor] | None, + controlnet_single_block_residuals: list[Tensor] | None, + ip_adapter_extensions: list[XLabsIPAdapterExtension], + regional_prompting_extension: RegionalPromptingExtension, + ) -> Tensor: + if img.ndim != 3 or txt.ndim != 3: + raise ValueError("Input img and txt tensors must have 3 dimensions.") + + # running on sequences img + img = self.img_in(img) + vec = self.time_in(timestep_embedding(timesteps, 256)) + if self.params.guidance_embed: + if guidance is None: + raise ValueError("Didn't get guidance strength for guidance distilled model.") + vec = vec + self.guidance_in(timestep_embedding(guidance, 256)) + vec = vec + self.vector_in(y) + txt = self.txt_in(txt) + + ids = torch.cat((txt_ids, img_ids), dim=1) + pe = self.pe_embedder(ids) + + # Validate double_block_residuals shape. + if controlnet_double_block_residuals is not None: + assert len(controlnet_double_block_residuals) == len(self.double_blocks) + for block_index, block in enumerate(self.double_blocks): + assert isinstance(block, DoubleStreamBlock) + img, txt = CustomDoubleStreamBlockProcessor.custom_double_block_forward( + timestep_index=timestep_index, + total_num_timesteps=total_num_timesteps, + block_index=block_index, + block=block, + img=img, + txt=txt, + vec=vec, + pe=pe, + ip_adapter_extensions=ip_adapter_extensions, + regional_prompting_extension=regional_prompting_extension, + ) + + if controlnet_double_block_residuals is not None: + img += controlnet_double_block_residuals[block_index] + + img = torch.cat((txt, img), 1) + + # Validate single_block_residuals shape. + if controlnet_single_block_residuals is not None: + assert len(controlnet_single_block_residuals) == len(self.single_blocks) + + for block_index, block in enumerate(self.single_blocks): + assert isinstance(block, SingleStreamBlock) + img = CustomSingleStreamBlockProcessor.custom_single_block_forward( + timestep_index=timestep_index, + total_num_timesteps=total_num_timesteps, + block_index=block_index, + block=block, + img=img, + vec=vec, + pe=pe, + regional_prompting_extension=regional_prompting_extension, + ) + + if controlnet_single_block_residuals is not None: + img[:, txt.shape[1] :, ...] += controlnet_single_block_residuals[block_index] + + img = img[:, txt.shape[1] :, ...] + + img = self.final_layer(img, vec) # (N, T, patch_size ** 2 * out_channels) + return img diff --git a/invokeai/backend/flux/modules/autoencoder.py b/invokeai/backend/flux/modules/autoencoder.py new file mode 100644 index 00000000000..6b072a82f63 --- /dev/null +++ b/invokeai/backend/flux/modules/autoencoder.py @@ -0,0 +1,324 @@ +# Initially pulled from https://github.com/black-forest-labs/flux + +from dataclasses import dataclass + +import torch +from einops import rearrange +from torch import Tensor, nn + + +@dataclass +class AutoEncoderParams: + resolution: int + in_channels: int + ch: int + out_ch: int + ch_mult: list[int] + num_res_blocks: int + z_channels: int + scale_factor: float + shift_factor: float + + +class AttnBlock(nn.Module): + def __init__(self, in_channels: int): + super().__init__() + self.in_channels = in_channels + + self.norm = nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True) + + self.q = nn.Conv2d(in_channels, in_channels, kernel_size=1) + self.k = nn.Conv2d(in_channels, in_channels, kernel_size=1) + self.v = nn.Conv2d(in_channels, in_channels, kernel_size=1) + self.proj_out = nn.Conv2d(in_channels, in_channels, kernel_size=1) + + def attention(self, h_: Tensor) -> Tensor: + h_ = self.norm(h_) + q = self.q(h_) + k = self.k(h_) + v = self.v(h_) + + b, c, h, w = q.shape + q = rearrange(q, "b c h w -> b 1 (h w) c").contiguous() + k = rearrange(k, "b c h w -> b 1 (h w) c").contiguous() + v = rearrange(v, "b c h w -> b 1 (h w) c").contiguous() + h_ = nn.functional.scaled_dot_product_attention(q, k, v) + + return rearrange(h_, "b 1 (h w) c -> b c h w", h=h, w=w, c=c, b=b) + + def forward(self, x: Tensor) -> Tensor: + return x + self.proj_out(self.attention(x)) + + +class ResnetBlock(nn.Module): + def __init__(self, in_channels: int, out_channels: int): + super().__init__() + self.in_channels = in_channels + out_channels = in_channels if out_channels is None else out_channels + self.out_channels = out_channels + + self.norm1 = nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True) + self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1) + self.norm2 = nn.GroupNorm(num_groups=32, num_channels=out_channels, eps=1e-6, affine=True) + self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1) + if self.in_channels != self.out_channels: + self.nin_shortcut = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0) + + def forward(self, x): + h = x + h = self.norm1(h) + h = torch.nn.functional.silu(h) + h = self.conv1(h) + + h = self.norm2(h) + h = torch.nn.functional.silu(h) + h = self.conv2(h) + + if self.in_channels != self.out_channels: + x = self.nin_shortcut(x) + + return x + h + + +class Downsample(nn.Module): + def __init__(self, in_channels: int): + super().__init__() + # no asymmetric padding in torch conv, must do it ourselves + self.conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=2, padding=0) + + def forward(self, x: Tensor): + pad = (0, 1, 0, 1) + x = nn.functional.pad(x, pad, mode="constant", value=0) + x = self.conv(x) + return x + + +class Upsample(nn.Module): + def __init__(self, in_channels: int): + super().__init__() + self.conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=1, padding=1) + + def forward(self, x: Tensor): + x = nn.functional.interpolate(x, scale_factor=2.0, mode="nearest") + x = self.conv(x) + return x + + +class Encoder(nn.Module): + def __init__( + self, + resolution: int, + in_channels: int, + ch: int, + ch_mult: list[int], + num_res_blocks: int, + z_channels: int, + ): + super().__init__() + self.ch = ch + self.num_resolutions = len(ch_mult) + self.num_res_blocks = num_res_blocks + self.resolution = resolution + self.in_channels = in_channels + # downsampling + self.conv_in = nn.Conv2d(in_channels, self.ch, kernel_size=3, stride=1, padding=1) + + curr_res = resolution + in_ch_mult = (1,) + tuple(ch_mult) + self.in_ch_mult = in_ch_mult + self.down = nn.ModuleList() + block_in = self.ch + for i_level in range(self.num_resolutions): + block = nn.ModuleList() + attn = nn.ModuleList() + block_in = ch * in_ch_mult[i_level] + block_out = ch * ch_mult[i_level] + for _ in range(self.num_res_blocks): + block.append(ResnetBlock(in_channels=block_in, out_channels=block_out)) + block_in = block_out + down = nn.Module() + down.block = block + down.attn = attn + if i_level != self.num_resolutions - 1: + down.downsample = Downsample(block_in) + curr_res = curr_res // 2 + self.down.append(down) + + # middle + self.mid = nn.Module() + self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in) + self.mid.attn_1 = AttnBlock(block_in) + self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in) + + # end + self.norm_out = nn.GroupNorm(num_groups=32, num_channels=block_in, eps=1e-6, affine=True) + self.conv_out = nn.Conv2d(block_in, 2 * z_channels, kernel_size=3, stride=1, padding=1) + + def forward(self, x: Tensor) -> Tensor: + # downsampling + hs = [self.conv_in(x)] + for i_level in range(self.num_resolutions): + for i_block in range(self.num_res_blocks): + h = self.down[i_level].block[i_block](hs[-1]) + if len(self.down[i_level].attn) > 0: + h = self.down[i_level].attn[i_block](h) + hs.append(h) + if i_level != self.num_resolutions - 1: + hs.append(self.down[i_level].downsample(hs[-1])) + + # middle + h = hs[-1] + h = self.mid.block_1(h) + h = self.mid.attn_1(h) + h = self.mid.block_2(h) + # end + h = self.norm_out(h) + h = torch.nn.functional.silu(h) + h = self.conv_out(h) + return h + + +class Decoder(nn.Module): + def __init__( + self, + ch: int, + out_ch: int, + ch_mult: list[int], + num_res_blocks: int, + in_channels: int, + resolution: int, + z_channels: int, + ): + super().__init__() + self.ch = ch + self.num_resolutions = len(ch_mult) + self.num_res_blocks = num_res_blocks + self.resolution = resolution + self.in_channels = in_channels + self.ffactor = 2 ** (self.num_resolutions - 1) + + # compute in_ch_mult, block_in and curr_res at lowest res + block_in = ch * ch_mult[self.num_resolutions - 1] + curr_res = resolution // 2 ** (self.num_resolutions - 1) + self.z_shape = (1, z_channels, curr_res, curr_res) + + # z to block_in + self.conv_in = nn.Conv2d(z_channels, block_in, kernel_size=3, stride=1, padding=1) + + # middle + self.mid = nn.Module() + self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in) + self.mid.attn_1 = AttnBlock(block_in) + self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in) + + # upsampling + self.up = nn.ModuleList() + for i_level in reversed(range(self.num_resolutions)): + block = nn.ModuleList() + attn = nn.ModuleList() + block_out = ch * ch_mult[i_level] + for _ in range(self.num_res_blocks + 1): + block.append(ResnetBlock(in_channels=block_in, out_channels=block_out)) + block_in = block_out + up = nn.Module() + up.block = block + up.attn = attn + if i_level != 0: + up.upsample = Upsample(block_in) + curr_res = curr_res * 2 + self.up.insert(0, up) # prepend to get consistent order + + # end + self.norm_out = nn.GroupNorm(num_groups=32, num_channels=block_in, eps=1e-6, affine=True) + self.conv_out = nn.Conv2d(block_in, out_ch, kernel_size=3, stride=1, padding=1) + + def forward(self, z: Tensor) -> Tensor: + # z to block_in + h = self.conv_in(z) + + # middle + h = self.mid.block_1(h) + h = self.mid.attn_1(h) + h = self.mid.block_2(h) + + # upsampling + for i_level in reversed(range(self.num_resolutions)): + for i_block in range(self.num_res_blocks + 1): + h = self.up[i_level].block[i_block](h) + if len(self.up[i_level].attn) > 0: + h = self.up[i_level].attn[i_block](h) + if i_level != 0: + h = self.up[i_level].upsample(h) + + # end + h = self.norm_out(h) + h = torch.nn.functional.silu(h) + h = self.conv_out(h) + return h + + +class DiagonalGaussian(nn.Module): + def __init__(self, chunk_dim: int = 1): + super().__init__() + self.chunk_dim = chunk_dim + + def forward(self, z: Tensor, sample: bool = True, generator: torch.Generator | None = None) -> Tensor: + mean, logvar = torch.chunk(z, 2, dim=self.chunk_dim) + if sample: + std = torch.exp(0.5 * logvar) + # Unfortunately, torch.randn_like(...) does not accept a generator argument at the time of writing, so we + # have to use torch.randn(...) instead. + return mean + std * torch.randn(size=mean.size(), generator=generator, dtype=mean.dtype, device=mean.device) + else: + return mean + + +class AutoEncoder(nn.Module): + def __init__(self, params: AutoEncoderParams): + super().__init__() + self.encoder = Encoder( + resolution=params.resolution, + in_channels=params.in_channels, + ch=params.ch, + ch_mult=params.ch_mult, + num_res_blocks=params.num_res_blocks, + z_channels=params.z_channels, + ) + self.decoder = Decoder( + resolution=params.resolution, + in_channels=params.in_channels, + ch=params.ch, + out_ch=params.out_ch, + ch_mult=params.ch_mult, + num_res_blocks=params.num_res_blocks, + z_channels=params.z_channels, + ) + self.reg = DiagonalGaussian() + + self.scale_factor = params.scale_factor + self.shift_factor = params.shift_factor + + def encode(self, x: Tensor, sample: bool = True, generator: torch.Generator | None = None) -> Tensor: + """Run VAE encoding on input tensor x. + + Args: + x (Tensor): Input image tensor. Shape: (batch_size, in_channels, height, width). + sample (bool, optional): If True, sample from the encoded distribution, else, return the distribution mean. + Defaults to True. + generator (torch.Generator | None, optional): Optional random number generator for reproducibility. + Defaults to None. + + Returns: + Tensor: Encoded latent tensor. Shape: (batch_size, z_channels, latent_height, latent_width). + """ + + z = self.reg(self.encoder(x), sample=sample, generator=generator) + z = self.scale_factor * (z - self.shift_factor) + return z + + def decode(self, z: Tensor) -> Tensor: + z = z / self.scale_factor + self.shift_factor + return self.decoder(z) + + def forward(self, x: Tensor) -> Tensor: + return self.decode(self.encode(x)) diff --git a/invokeai/backend/flux/modules/conditioner.py b/invokeai/backend/flux/modules/conditioner.py new file mode 100644 index 00000000000..d48d78cd4a1 --- /dev/null +++ b/invokeai/backend/flux/modules/conditioner.py @@ -0,0 +1,44 @@ +# Initially pulled from https://github.com/black-forest-labs/flux + +from torch import Tensor, nn +from transformers import PreTrainedModel, PreTrainedTokenizer, PreTrainedTokenizerFast + +from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device + + +class HFEncoder(nn.Module): + def __init__( + self, + encoder: PreTrainedModel, + tokenizer: PreTrainedTokenizer | PreTrainedTokenizerFast, + is_clip: bool, + max_length: int, + ): + super().__init__() + self.max_length = max_length + self.is_clip = is_clip + self.output_key = "pooler_output" if self.is_clip else "last_hidden_state" + self.tokenizer = tokenizer + self.hf_module = encoder + self.hf_module = self.hf_module.eval().requires_grad_(False) + + def forward(self, text: list[str]) -> Tensor: + batch_encoding = self.tokenizer( + text, + truncation=True, + max_length=self.max_length, + return_length=False, + return_overflowing_tokens=False, + padding="max_length", + return_tensors="pt", + ) + + # Move inputs to the same device as the model to support cpu_only models + model_device = get_effective_device(self.hf_module) + + outputs = self.hf_module( + input_ids=batch_encoding["input_ids"].to(model_device), + attention_mask=None, + output_hidden_states=False, + ) + return outputs[self.output_key] diff --git a/invokeai/backend/flux/modules/layers.py b/invokeai/backend/flux/modules/layers.py new file mode 100644 index 00000000000..878ee34d413 --- /dev/null +++ b/invokeai/backend/flux/modules/layers.py @@ -0,0 +1,250 @@ +# Initially pulled from https://github.com/black-forest-labs/flux + +import math +from dataclasses import dataclass + +import torch +from einops import rearrange +from torch import Tensor, nn + +from invokeai.backend.flux.math import attention, rope + + +class EmbedND(nn.Module): + def __init__(self, dim: int, theta: int, axes_dim: list[int]): + super().__init__() + self.dim = dim + self.theta = theta + self.axes_dim = axes_dim + + def forward(self, ids: Tensor) -> Tensor: + n_axes = ids.shape[-1] + emb = torch.cat( + [rope(ids[..., i], self.axes_dim[i], self.theta) for i in range(n_axes)], + dim=-3, + ) + + return emb.unsqueeze(1) + + +def timestep_embedding(t: Tensor, dim, max_period=10000, time_factor: float = 1000.0): + """ + Create sinusoidal timestep embeddings. + :param t: a 1-D Tensor of N indices, one per batch element. + These may be fractional. + :param dim: the dimension of the output. + :param max_period: controls the minimum frequency of the embeddings. + :return: an (N, D) Tensor of positional embeddings. + """ + t = time_factor * t + half = dim // 2 + freqs = torch.exp(-math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half).to(t.device) + + args = t[:, None].float() * freqs[None] + embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1) + if dim % 2: + embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1) + if torch.is_floating_point(t): + embedding = embedding.to(t) + return embedding + + +class MLPEmbedder(nn.Module): + def __init__(self, in_dim: int, hidden_dim: int): + super().__init__() + self.in_layer = nn.Linear(in_dim, hidden_dim, bias=True) + self.silu = nn.SiLU() + self.out_layer = nn.Linear(hidden_dim, hidden_dim, bias=True) + + def forward(self, x: Tensor) -> Tensor: + return self.out_layer(self.silu(self.in_layer(x))) + + +class RMSNorm(torch.nn.Module): + def __init__(self, dim: int): + super().__init__() + self.scale = nn.Parameter(torch.ones(dim)) + + def forward(self, x: Tensor): + return torch.nn.functional.rms_norm(x, self.scale.shape, self.scale, eps=1e-6) + + +class QKNorm(torch.nn.Module): + def __init__(self, dim: int): + super().__init__() + self.query_norm = RMSNorm(dim) + self.key_norm = RMSNorm(dim) + + def forward(self, q: Tensor, k: Tensor, v: Tensor) -> tuple[Tensor, Tensor]: + q = self.query_norm(q) + k = self.key_norm(k) + return q.to(v), k.to(v) + + +class SelfAttention(nn.Module): + def __init__(self, dim: int, num_heads: int = 8, qkv_bias: bool = False): + super().__init__() + self.num_heads = num_heads + head_dim = dim // num_heads + + self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) + self.norm = QKNorm(head_dim) + self.proj = nn.Linear(dim, dim) + + def forward(self, x: Tensor, pe: Tensor) -> Tensor: + qkv = self.qkv(x) + q, k, v = rearrange(qkv, "B L (K H D) -> K B H L D", K=3, H=self.num_heads) + q, k = self.norm(q, k, v) + x = attention(q, k, v, pe=pe) + x = self.proj(x) + return x + + +@dataclass +class ModulationOut: + shift: Tensor + scale: Tensor + gate: Tensor + + +class Modulation(nn.Module): + def __init__(self, dim: int, double: bool): + super().__init__() + self.is_double = double + self.multiplier = 6 if double else 3 + self.lin = nn.Linear(dim, self.multiplier * dim, bias=True) + + def forward(self, vec: Tensor) -> tuple[ModulationOut, ModulationOut | None]: + out = self.lin(nn.functional.silu(vec))[:, None, :].chunk(self.multiplier, dim=-1) + + return ( + ModulationOut(*out[:3]), + ModulationOut(*out[3:]) if self.is_double else None, + ) + + +class DoubleStreamBlock(nn.Module): + def __init__(self, hidden_size: int, num_heads: int, mlp_ratio: float, qkv_bias: bool = False): + super().__init__() + + mlp_hidden_dim = int(hidden_size * mlp_ratio) + self.num_heads = num_heads + self.hidden_size = hidden_size + self.img_mod = Modulation(hidden_size, double=True) + self.img_norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.img_attn = SelfAttention(dim=hidden_size, num_heads=num_heads, qkv_bias=qkv_bias) + + self.img_norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.img_mlp = nn.Sequential( + nn.Linear(hidden_size, mlp_hidden_dim, bias=True), + nn.GELU(approximate="tanh"), + nn.Linear(mlp_hidden_dim, hidden_size, bias=True), + ) + + self.txt_mod = Modulation(hidden_size, double=True) + self.txt_norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.txt_attn = SelfAttention(dim=hidden_size, num_heads=num_heads, qkv_bias=qkv_bias) + + self.txt_norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.txt_mlp = nn.Sequential( + nn.Linear(hidden_size, mlp_hidden_dim, bias=True), + nn.GELU(approximate="tanh"), + nn.Linear(mlp_hidden_dim, hidden_size, bias=True), + ) + + def forward(self, img: Tensor, txt: Tensor, vec: Tensor, pe: Tensor) -> tuple[Tensor, Tensor]: + img_mod1, img_mod2 = self.img_mod(vec) + txt_mod1, txt_mod2 = self.txt_mod(vec) + + # prepare image for attention + img_modulated = self.img_norm1(img) + img_modulated = (1 + img_mod1.scale) * img_modulated + img_mod1.shift + img_qkv = self.img_attn.qkv(img_modulated) + img_q, img_k, img_v = rearrange(img_qkv, "B L (K H D) -> K B H L D", K=3, H=self.num_heads) + img_q, img_k = self.img_attn.norm(img_q, img_k, img_v) + + # prepare txt for attention + txt_modulated = self.txt_norm1(txt) + txt_modulated = (1 + txt_mod1.scale) * txt_modulated + txt_mod1.shift + txt_qkv = self.txt_attn.qkv(txt_modulated) + txt_q, txt_k, txt_v = rearrange(txt_qkv, "B L (K H D) -> K B H L D", K=3, H=self.num_heads) + txt_q, txt_k = self.txt_attn.norm(txt_q, txt_k, txt_v) + + # run actual attention + q = torch.cat((txt_q, img_q), dim=2) + k = torch.cat((txt_k, img_k), dim=2) + v = torch.cat((txt_v, img_v), dim=2) + + attn = attention(q, k, v, pe=pe) + txt_attn, img_attn = attn[:, : txt.shape[1]], attn[:, txt.shape[1] :] + + # calculate the img bloks + img = img + img_mod1.gate * self.img_attn.proj(img_attn) + img = img + img_mod2.gate * self.img_mlp((1 + img_mod2.scale) * self.img_norm2(img) + img_mod2.shift) + + # calculate the txt bloks + txt = txt + txt_mod1.gate * self.txt_attn.proj(txt_attn) + txt = txt + txt_mod2.gate * self.txt_mlp((1 + txt_mod2.scale) * self.txt_norm2(txt) + txt_mod2.shift) + return img, txt + + +class SingleStreamBlock(nn.Module): + """ + A DiT block with parallel linear layers as described in + https://arxiv.org/abs/2302.05442 and adapted modulation interface. + """ + + def __init__( + self, + hidden_size: int, + num_heads: int, + mlp_ratio: float = 4.0, + qk_scale: float | None = None, + ): + super().__init__() + self.hidden_dim = hidden_size + self.num_heads = num_heads + head_dim = hidden_size // num_heads + self.scale = qk_scale or head_dim**-0.5 + + self.mlp_hidden_dim = int(hidden_size * mlp_ratio) + # qkv and mlp_in + self.linear1 = nn.Linear(hidden_size, hidden_size * 3 + self.mlp_hidden_dim) + # proj and mlp_out + self.linear2 = nn.Linear(hidden_size + self.mlp_hidden_dim, hidden_size) + + self.norm = QKNorm(head_dim) + + self.hidden_size = hidden_size + self.pre_norm = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + + self.mlp_act = nn.GELU(approximate="tanh") + self.modulation = Modulation(hidden_size, double=False) + + def forward(self, x: Tensor, vec: Tensor, pe: Tensor) -> Tensor: + mod, _ = self.modulation(vec) + x_mod = (1 + mod.scale) * self.pre_norm(x) + mod.shift + qkv, mlp = torch.split(self.linear1(x_mod), [3 * self.hidden_size, self.mlp_hidden_dim], dim=-1) + + q, k, v = rearrange(qkv, "B L (K H D) -> K B H L D", K=3, H=self.num_heads) + q, k = self.norm(q, k, v) + + # compute attention + attn = attention(q, k, v, pe=pe) + # compute activation in mlp stream, cat again and run second linear layer + output = self.linear2(torch.cat((attn, self.mlp_act(mlp)), 2)) + return x + mod.gate * output + + +class LastLayer(nn.Module): + def __init__(self, hidden_size: int, patch_size: int, out_channels: int): + super().__init__() + self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.linear = nn.Linear(hidden_size, patch_size * patch_size * out_channels, bias=True) + self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 2 * hidden_size, bias=True)) + + def forward(self, x: Tensor, vec: Tensor) -> Tensor: + shift, scale = self.adaLN_modulation(vec).chunk(2, dim=1) + x = (1 + scale[:, None, :]) * self.norm_final(x) + shift[:, None, :] + x = self.linear(x) + return x diff --git a/invokeai/backend/flux/redux/flux_redux_model.py b/invokeai/backend/flux/redux/flux_redux_model.py new file mode 100644 index 00000000000..218ebfcdb27 --- /dev/null +++ b/invokeai/backend/flux/redux/flux_redux_model.py @@ -0,0 +1,17 @@ +import torch + +# This model definition is based on: +# https://github.com/black-forest-labs/flux/blob/716724eb276d94397be99710a0a54d352664e23b/src/flux/modules/image_embedders.py#L66 + + +class FluxReduxModel(torch.nn.Module): + def __init__(self, redux_dim: int = 1152, txt_in_features: int = 4096) -> None: + super().__init__() + + self.redux_dim = redux_dim + + self.redux_up = torch.nn.Linear(redux_dim, txt_in_features * 3) + self.redux_down = torch.nn.Linear(txt_in_features * 3, txt_in_features) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.redux_down(torch.nn.functional.silu(self.redux_up(x))) diff --git a/invokeai/backend/flux/redux/flux_redux_state_dict_utils.py b/invokeai/backend/flux/redux/flux_redux_state_dict_utils.py new file mode 100644 index 00000000000..83e96d38451 --- /dev/null +++ b/invokeai/backend/flux/redux/flux_redux_state_dict_utils.py @@ -0,0 +1,11 @@ +from typing import Any + + +def is_state_dict_likely_flux_redux(state_dict: dict[str | int, Any]) -> bool: + """Checks if the provided state dict is likely a FLUX Redux model.""" + + expected_keys = {"redux_down.bias", "redux_down.weight", "redux_up.bias", "redux_up.weight"} + if set(state_dict.keys()) == expected_keys: + return True + + return False diff --git a/invokeai/backend/flux/sampling_utils.py b/invokeai/backend/flux/sampling_utils.py new file mode 100644 index 00000000000..be81d6458d8 --- /dev/null +++ b/invokeai/backend/flux/sampling_utils.py @@ -0,0 +1,186 @@ +# Initially pulled from https://github.com/black-forest-labs/flux + +import math +from typing import Callable + +import torch +from einops import rearrange, repeat + + +def get_noise( + num_samples: int, + height: int, + width: int, + device: torch.device, + dtype: torch.dtype, + seed: int, +): + # 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, + 16, + # allow for packing + 2 * math.ceil(height / 16), + 2 * math.ceil(width / 16), + device=rand_device, + dtype=rand_dtype, + generator=torch.Generator(device=rand_device).manual_seed(seed), + ).to(device=device, dtype=dtype) + + +def time_shift(mu: float, sigma: float, t: torch.Tensor) -> torch.Tensor: + return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma) + + +def get_lin_function(x1: float = 256, y1: float = 0.5, x2: float = 4096, y2: float = 1.15) -> Callable[[float], float]: + m = (y2 - y1) / (x2 - x1) + b = y1 - m * x1 + return lambda x: m * x + b + + +def get_schedule( + num_steps: int, + image_seq_len: int, + base_shift: float = 0.5, + max_shift: float = 1.15, + shift: bool = True, +) -> list[float]: + # extra step for zero + timesteps = torch.linspace(1, 0, num_steps + 1) + + # shifting the schedule to favor high timesteps for higher signal images + if shift: + # estimate mu based on linear estimation between two points + mu = get_lin_function(y1=base_shift, y2=max_shift)(image_seq_len) + timesteps = time_shift(mu, 1.0, timesteps) + + return timesteps.tolist() + + +def _find_last_index_ge_val(timesteps: list[float], val: float, eps: float = 1e-6) -> int: + """Find the last index in timesteps that is >= val. + + We use epsilon-close equality to avoid potential floating point errors. + """ + idx = len(list(filter(lambda t: t >= (val - eps), timesteps))) - 1 + assert idx >= 0 + return idx + + +def clip_timestep_schedule(timesteps: list[float], denoising_start: float, denoising_end: float) -> list[float]: + """Clip the timestep schedule to the denoising range. + + Args: + timesteps (list[float]): The original timestep schedule: [1.0, ..., 0.0]. + denoising_start (float): A value in [0, 1] specifying the start of the denoising process. E.g. a value of 0.2 + would mean that the denoising process start at the last timestep in the schedule >= 0.8. + denoising_end (float): A value in [0, 1] specifying the end of the denoising process. E.g. a value of 0.8 would + mean that the denoising process end at the last timestep in the schedule >= 0.2. + + Returns: + list[float]: The clipped timestep schedule. + """ + assert 0.0 <= denoising_start <= 1.0 + assert 0.0 <= denoising_end <= 1.0 + assert denoising_start <= denoising_end + + t_start_val = 1.0 - denoising_start + t_end_val = 1.0 - denoising_end + + t_start_idx = _find_last_index_ge_val(timesteps, t_start_val) + t_end_idx = _find_last_index_ge_val(timesteps, t_end_val) + + clipped_timesteps = timesteps[t_start_idx : t_end_idx + 1] + + return clipped_timesteps + + +def clip_timestep_schedule_fractional( + timesteps: list[float], denoising_start: float, denoising_end: float +) -> list[float]: + """Clip the timestep schedule to the denoising range. Insert new timesteps to exactly match the desired denoising + range. (A fractional version of clip_timestep_schedule().) + + Args: + timesteps (list[float]): The original timestep schedule: [1.0, ..., 0.0]. + denoising_start (float): A value in [0, 1] specifying the start of the denoising process. E.g. a value of 0.2 + would mean that the denoising process start at t=0.8. + denoising_end (float): A value in [0, 1] specifying the end of the denoising process. E.g. a value of 0.8 would + mean that the denoising process ends at t=0.2. + + Returns: + list[float]: The clipped timestep schedule. + """ + assert 0.0 <= denoising_start <= 1.0 + assert 0.0 <= denoising_end <= 1.0 + assert denoising_start <= denoising_end + + t_start_val = 1.0 - denoising_start + t_end_val = 1.0 - denoising_end + + t_start_idx = _find_last_index_ge_val(timesteps, t_start_val) + t_end_idx = _find_last_index_ge_val(timesteps, t_end_val) + + clipped_timesteps = timesteps[t_start_idx : t_end_idx + 1] + + # We know that clipped_timesteps[0] >= t_start_val. Replace clipped_timesteps[0] with t_start_val. + clipped_timesteps[0] = t_start_val + + # We know that clipped_timesteps[-1] >= t_end_val. If clipped_timesteps[-1] > t_end_val, add another step to + # t_end_val. + eps = 1e-6 + if clipped_timesteps[-1] > t_end_val + eps: + clipped_timesteps.append(t_end_val) + + return clipped_timesteps + + +def unpack(x: torch.Tensor, height: int, width: int) -> torch.Tensor: + """Unpack flat array of patch embeddings to latent image.""" + return rearrange( + x, + "b (h w) (c ph pw) -> b c (h ph) (w pw)", + h=math.ceil(height / 16), + w=math.ceil(width / 16), + ph=2, + pw=2, + ) + + +def pack(x: torch.Tensor) -> torch.Tensor: + """Pack latent image to flattented array of patch embeddings.""" + # Pixel unshuffle with a scale of 2, and flatten the height/width dimensions to get an array of patches. + return rearrange(x, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=2, pw=2) + + +def generate_img_ids(h: int, w: int, batch_size: int, device: torch.device, dtype: torch.dtype) -> torch.Tensor: + """Generate tensor of image position ids. + + Args: + h (int): Height of image in latent space. + w (int): Width of image in latent space. + batch_size (int): Batch size. + device (torch.device): Device. + dtype (torch.dtype): dtype. + + Returns: + torch.Tensor: Image position ids. + """ + + if device.type == "mps": + orig_dtype = dtype + dtype = torch.float16 + + img_ids = torch.zeros(h // 2, w // 2, 3, device=device, dtype=dtype) + # Set batch offset to 0 for main image tokens + img_ids[..., 0] = 0 + img_ids[..., 1] = img_ids[..., 1] + torch.arange(h // 2, device=device, dtype=dtype)[:, None] + img_ids[..., 2] = img_ids[..., 2] + torch.arange(w // 2, device=device, dtype=dtype)[None, :] + img_ids = repeat(img_ids, "h w c -> b (h w) c", b=batch_size) + + if device.type == "mps": + img_ids = img_ids.to(orig_dtype) + + return img_ids diff --git a/invokeai/backend/flux/schedulers.py b/invokeai/backend/flux/schedulers.py new file mode 100644 index 00000000000..b35658d20f3 --- /dev/null +++ b/invokeai/backend/flux/schedulers.py @@ -0,0 +1,131 @@ +"""Flow Matching scheduler definitions and mapping. + +This module provides the scheduler types and mapping for Flow Matching models +(Flux and Z-Image), supporting multiple schedulers from the diffusers library. +""" + +from typing import Any, Literal, Type + +from diffusers import ( + DPMSolverMultistepScheduler, + FlowMatchEulerDiscreteScheduler, + FlowMatchHeunDiscreteScheduler, +) +from diffusers.schedulers.scheduling_utils import SchedulerMixin + +from invokeai.backend.rectified_flow.er_sde_scheduler import ERSDEScheduler + +# Note: FlowMatchLCMScheduler may not be available in all diffusers versions +try: + from diffusers import FlowMatchLCMScheduler + + _HAS_LCM = True +except ImportError: + _HAS_LCM = False + +# Scheduler name literal type for type checking +FLUX_SCHEDULER_NAME_VALUES = Literal["euler", "heun", "lcm"] + +# Human-readable labels for the UI +FLUX_SCHEDULER_LABELS: dict[str, str] = { + "euler": "Euler", + "heun": "Heun (2nd order)", + "lcm": "LCM", +} + +# Mapping from scheduler names to scheduler classes +FLUX_SCHEDULER_MAP: dict[str, Type[SchedulerMixin]] = { + "euler": FlowMatchEulerDiscreteScheduler, + "heun": FlowMatchHeunDiscreteScheduler, +} + +if _HAS_LCM: + FLUX_SCHEDULER_MAP["lcm"] = FlowMatchLCMScheduler + + +# Z-Image scheduler types (Flow Matching schedulers) +# Note: Z-Image-Turbo is optimized for ~8 steps with Euler, LCM can also work. +# Z-Image Base (undistilled) should only use Euler or Heun (LCM not supported for undistilled models). +ZIMAGE_SCHEDULER_NAME_VALUES = Literal["euler", "heun", "lcm"] + +# Human-readable labels for the UI +ZIMAGE_SCHEDULER_LABELS: dict[str, str] = { + "euler": "Euler", + "heun": "Heun (2nd order)", + "lcm": "LCM", +} + +# Mapping from scheduler names to scheduler classes +ZIMAGE_SCHEDULER_MAP: dict[str, Type[SchedulerMixin]] = { + "euler": FlowMatchEulerDiscreteScheduler, + "heun": FlowMatchHeunDiscreteScheduler, +} + +if _HAS_LCM: + ZIMAGE_SCHEDULER_MAP["lcm"] = FlowMatchLCMScheduler + + +# Anima scheduler types. +# Anima uses rectified flow with shift=3.0. The driver passes pre-shifted sigmas via +# set_timesteps(sigmas=...) when the scheduler accepts that signature. For those, the +# entry carries shift=1.0 to avoid double-shifting (the scheduler uses our sigmas verbatim). +# Schedulers that don't accept sigmas= (Heun, DPM++ on diffusers 0.35.1) build their own +# internal schedule, so they need shift=ANIMA_SHIFT/flow_shift=ANIMA_SHIFT in kwargs to match +# Anima's reference loglinear schedule. + +# Fixed shift factor for the Anima rectified-flow noise schedule. +ANIMA_SHIFT = 3.0 + +ANIMA_SCHEDULER_NAME_VALUES = Literal["euler", "heun", "dpmpp_2m", "dpmpp_2m_sde", "er_sde", "lcm"] + +ANIMA_SCHEDULER_LABELS: dict[str, str] = { + "euler": "Euler", + "heun": "Heun (2nd order)", + "dpmpp_2m": "DPM++ 2M", + "dpmpp_2m_sde": "DPM++ 2M SDE", + "er_sde": "ER-SDE", + "lcm": "LCM", +} + +# When adding a new Anima scheduler: add to all three of NAME_VALUES, LABELS, +# and this MAP. The MAP entry is `(SchedulerClass, scheduler_kwargs)`. For +# rectified-flow schedulers, set `use_flow_sigmas=True` and use +# `prediction_type="flow_prediction"`. If the scheduler accepts set_timesteps(sigmas=...), +# use shift=1.0 (driver passes pre-shifted sigmas); otherwise use shift=ANIMA_SHIFT +# so the scheduler builds the correct internal schedule. +ANIMA_SCHEDULER_MAP: dict[str, tuple[Type[SchedulerMixin], dict[str, Any]]] = { + "euler": (FlowMatchEulerDiscreteScheduler, {"shift": 1.0}), + "heun": (FlowMatchHeunDiscreteScheduler, {"shift": ANIMA_SHIFT}), + "dpmpp_2m": ( + DPMSolverMultistepScheduler, + { + "prediction_type": "flow_prediction", + "use_flow_sigmas": True, + "flow_shift": ANIMA_SHIFT, + "solver_order": 2, + }, + ), + "dpmpp_2m_sde": ( + DPMSolverMultistepScheduler, + { + "prediction_type": "flow_prediction", + "use_flow_sigmas": True, + "flow_shift": ANIMA_SHIFT, + "algorithm_type": "sde-dpmsolver++", + "solver_order": 2, + }, + ), + "er_sde": ( + ERSDEScheduler, + { + "prediction_type": "flow_prediction", + "use_flow_sigmas": True, + "flow_shift": ANIMA_SHIFT, + "solver_order": 3, + "stochastic": True, + }, + ), +} + +if _HAS_LCM: + ANIMA_SCHEDULER_MAP["lcm"] = (FlowMatchLCMScheduler, {"shift": 1.0}) diff --git a/invokeai/backend/flux/text_conditioning.py b/invokeai/backend/flux/text_conditioning.py new file mode 100644 index 00000000000..f2a9d71a37a --- /dev/null +++ b/invokeai/backend/flux/text_conditioning.py @@ -0,0 +1,43 @@ +from dataclasses import dataclass + +import torch + +from invokeai.backend.stable_diffusion.diffusion.conditioning_data import Range + + +@dataclass +class FluxTextConditioning: + t5_embeddings: torch.Tensor + clip_embeddings: torch.Tensor + # If mask is None, the prompt is a global prompt. + mask: torch.Tensor | None + + +@dataclass +class FluxReduxConditioning: + redux_embeddings: torch.Tensor + # If mask is None, the prompt is a global prompt. + mask: torch.Tensor | None + + +@dataclass +class FluxRegionalTextConditioning: + # Concatenated text embeddings. + # Shape: (1, concatenated_txt_seq_len, 4096) + t5_embeddings: torch.Tensor + # Shape: (1, concatenated_txt_seq_len, 3) + t5_txt_ids: torch.Tensor + + # Global CLIP embeddings. + # Shape: (1, 768) + clip_embeddings: torch.Tensor + + # A binary mask indicating the regions of the image that the prompt should be applied to. If None, the prompt is a + # global prompt. + # image_masks[i] is the mask for the ith prompt. + # image_masks[i] has shape (1, image_seq_len) and dtype torch.bool. + image_masks: list[torch.Tensor | None] + + # List of ranges that represent the embedding ranges for each mask. + # t5_embedding_ranges[i] contains the range of the t5 embeddings that correspond to image_masks[i]. + t5_embedding_ranges: list[Range] diff --git a/invokeai/backend/flux/util.py b/invokeai/backend/flux/util.py new file mode 100644 index 00000000000..81b10a913ac --- /dev/null +++ b/invokeai/backend/flux/util.py @@ -0,0 +1,195 @@ +# Initially pulled from https://github.com/black-forest-labs/flux + +from dataclasses import dataclass +from typing import Literal + +from invokeai.backend.flux.model import FluxParams +from invokeai.backend.flux.modules.autoencoder import AutoEncoderParams +from invokeai.backend.model_manager.taxonomy import AnyVariant, Flux2VariantType, FluxVariantType + + +@dataclass +class ModelSpec: + params: FluxParams + ae_params: AutoEncoderParams + ckpt_path: str | None + ae_path: str | None + repo_id: str | None + repo_flow: str | None + repo_ae: str | None + + +# Preferred resolutions for Kontext models to avoid tiling artifacts +# These are the specific resolutions the model was trained on +PREFERED_KONTEXT_RESOLUTIONS = [ + (672, 1568), + (688, 1504), + (720, 1456), + (752, 1392), + (800, 1328), + (832, 1248), + (880, 1184), + (944, 1104), + (1024, 1024), + (1104, 944), + (1184, 880), + (1248, 832), + (1328, 800), + (1392, 752), + (1456, 720), + (1504, 688), + (1568, 672), +] + + +_flux_max_seq_lengths: dict[AnyVariant, Literal[256, 512]] = { + FluxVariantType.Dev: 512, + FluxVariantType.DevFill: 512, + FluxVariantType.Schnell: 256, + Flux2VariantType.Klein4B: 512, + Flux2VariantType.Klein9B: 512, +} + + +def get_flux_max_seq_length(variant: AnyVariant): + try: + return _flux_max_seq_lengths[variant] + except KeyError: + raise ValueError(f"Unknown variant for FLUX max seq len: {variant}") + + +_flux_ae_params = AutoEncoderParams( + resolution=256, + in_channels=3, + ch=128, + out_ch=3, + ch_mult=[1, 2, 4, 4], + num_res_blocks=2, + z_channels=16, + scale_factor=0.3611, + shift_factor=0.1159, +) + + +def get_flux_ae_params() -> AutoEncoderParams: + return _flux_ae_params + + +_flux_transformer_params: dict[AnyVariant, FluxParams] = { + FluxVariantType.Dev: FluxParams( + in_channels=64, + vec_in_dim=768, + context_in_dim=4096, + hidden_size=3072, + mlp_ratio=4.0, + num_heads=24, + depth=19, + depth_single_blocks=38, + axes_dim=[16, 56, 56], + theta=10_000, + qkv_bias=True, + guidance_embed=True, + ), + FluxVariantType.Schnell: FluxParams( + in_channels=64, + vec_in_dim=768, + context_in_dim=4096, + hidden_size=3072, + mlp_ratio=4.0, + num_heads=24, + depth=19, + depth_single_blocks=38, + axes_dim=[16, 56, 56], + theta=10_000, + qkv_bias=True, + guidance_embed=False, + ), + FluxVariantType.DevFill: FluxParams( + in_channels=384, + out_channels=64, + vec_in_dim=768, + context_in_dim=4096, + hidden_size=3072, + mlp_ratio=4.0, + num_heads=24, + depth=19, + depth_single_blocks=38, + axes_dim=[16, 56, 56], + theta=10_000, + qkv_bias=True, + guidance_embed=True, + ), + # Flux2 Klein 4B uses Qwen3 4B text encoder with stacked embeddings from layers [9, 18, 27] + # The context_in_dim is 3 * hidden_size of Qwen3 (3 * 2560 = 7680) + Flux2VariantType.Klein4B: FluxParams( + in_channels=64, + vec_in_dim=2560, # Qwen3-4B hidden size (used for pooled output) + context_in_dim=7680, # 3 layers * 2560 = 7680 for Qwen3-4B + hidden_size=3072, + mlp_ratio=4.0, + num_heads=24, + depth=19, + depth_single_blocks=38, + axes_dim=[16, 56, 56], + theta=10_000, + qkv_bias=True, + guidance_embed=False, + ), + # Flux2 Klein 4B Base is the undistilled foundation model. It shares the same + # architecture as Klein 4B (distilled) and reports guidance_embeds=False in its + # HF transformer config - classical CFG (external negative pass) is the guidance mechanism. + Flux2VariantType.Klein4BBase: FluxParams( + in_channels=64, + vec_in_dim=2560, # Qwen3-4B hidden size (used for pooled output) + context_in_dim=7680, # 3 layers * 2560 = 7680 for Qwen3-4B + hidden_size=3072, + mlp_ratio=4.0, + num_heads=24, + depth=19, + depth_single_blocks=38, + axes_dim=[16, 56, 56], + theta=10_000, + qkv_bias=True, + guidance_embed=False, + ), + # Flux2 Klein 9B uses Qwen3 8B text encoder with stacked embeddings from layers [9, 18, 27] + # The context_in_dim is 3 * hidden_size of Qwen3 (3 * 4096 = 12288) + Flux2VariantType.Klein9B: FluxParams( + in_channels=64, + vec_in_dim=4096, # Qwen3-8B hidden size (used for pooled output) + context_in_dim=12288, # 3 layers * 4096 = 12288 for Qwen3-8B + hidden_size=3072, + mlp_ratio=4.0, + num_heads=24, + depth=19, + depth_single_blocks=38, + axes_dim=[16, 56, 56], + theta=10_000, + qkv_bias=True, + guidance_embed=False, + ), + # Flux2 Klein 9B Base is the undistilled foundation model. It shares the same + # architecture as Klein 9B (distilled) and reports guidance_embeds=False in its + # HF transformer config - the guidance scalar is inert for all Klein variants. + Flux2VariantType.Klein9BBase: FluxParams( + in_channels=64, + vec_in_dim=4096, # Qwen3-8B hidden size (used for pooled output) + context_in_dim=12288, # 3 layers * 4096 = 12288 for Qwen3-8B + hidden_size=3072, + mlp_ratio=4.0, + num_heads=24, + depth=19, + depth_single_blocks=38, + axes_dim=[16, 56, 56], + theta=10_000, + qkv_bias=True, + guidance_embed=False, + ), +} + + +def get_flux_transformers_params(variant: AnyVariant): + try: + return _flux_transformer_params[variant] + except KeyError: + raise ValueError(f"Unknown variant for FLUX transformer params: {variant}") diff --git a/invokeai/backend/flux2/__init__.py b/invokeai/backend/flux2/__init__.py new file mode 100644 index 00000000000..cabb51efb9b --- /dev/null +++ b/invokeai/backend/flux2/__init__.py @@ -0,0 +1,4 @@ +"""FLUX.2 backend modules. + +This package contains modules specific to FLUX.2 models (e.g., Klein). +""" diff --git a/invokeai/backend/flux2/denoise.py b/invokeai/backend/flux2/denoise.py new file mode 100644 index 00000000000..2ff66236ce8 --- /dev/null +++ b/invokeai/backend/flux2/denoise.py @@ -0,0 +1,302 @@ +"""Flux2 Klein Denoising Function. + +This module provides the denoising function for FLUX.2 Klein models, +which use Qwen3 as the text encoder instead of CLIP+T5. +""" + +import inspect +import math +from typing import Any, Callable + +import numpy as np +import torch +from tqdm import tqdm + +from invokeai.backend.rectified_flow.rectified_flow_inpaint_extension import RectifiedFlowInpaintExtension +from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState + + +def denoise( + model: torch.nn.Module, + # model input + img: torch.Tensor, + img_ids: torch.Tensor, + txt: torch.Tensor, + txt_ids: torch.Tensor, + # sampling parameters + timesteps: list[float], + step_callback: Callable[[PipelineIntermediateState], None], + guidance: float, + cfg_scale: list[float], + # Negative conditioning for CFG + neg_txt: torch.Tensor | None = None, + neg_txt_ids: torch.Tensor | None = None, + # Scheduler for stepping (e.g., FlowMatchEulerDiscreteScheduler, FlowMatchHeunDiscreteScheduler) + scheduler: Any = None, + # Dynamic shifting parameter for FLUX.2 Klein (computed from image resolution) + mu: float | None = None, + # Inpainting extension for merging latents during denoising + inpaint_extension: RectifiedFlowInpaintExtension | None = None, + # Reference image conditioning (multi-reference image editing) + img_cond_seq: torch.Tensor | None = None, + img_cond_seq_ids: torch.Tensor | None = None, +) -> torch.Tensor: + """Denoise latents using a FLUX.2 Klein transformer model. + + This is a simplified denoise function for FLUX.2 Klein models that uses + the diffusers Flux2Transformer2DModel interface. + + All current FLUX.2 Klein variants (4B, 4B Base, 9B, 9B Base) have guidance_embeds=False + in their HF transformer config (or absent/zeroed projection weights), so the guidance + value is passed but effectively ignored by the model. The argument is retained for + node-graph compatibility and future variants that may ship trained guidance projections. + CFG is applied externally using negative conditioning when cfg_scale != 1.0. + + Args: + model: The Flux2Transformer2DModel from diffusers. + img: Packed latent image tensor of shape (B, seq_len, channels). + img_ids: Image position IDs tensor. + txt: Text encoder hidden states (Qwen3 embeddings). + txt_ids: Text position IDs tensor. + timesteps: List of timesteps for denoising schedule (linear sigmas from 1.0 to 1/n). + step_callback: Callback function for progress updates. + guidance: Guidance strength. Inert for all current FLUX.2 Klein variants + (their guidance_embeds projection weights are absent/zero). + cfg_scale: List of CFG scale values per step. + neg_txt: Negative text embeddings for CFG (optional). + neg_txt_ids: Negative text position IDs (optional). + scheduler: Optional diffusers scheduler (Euler, Heun, LCM). If None, uses manual Euler. + mu: Dynamic shifting parameter computed from image resolution. Required when scheduler + has use_dynamic_shifting=True. + + Returns: + Denoised latent tensor. + """ + total_steps = len(timesteps) - 1 + + # Store original sequence length for extracting output later (before concatenating reference images) + original_seq_len = img.shape[1] + + # Concatenate reference image conditioning if provided (multi-reference image editing) + if img_cond_seq is not None and img_cond_seq_ids is not None: + img = torch.cat([img, img_cond_seq], dim=1) + img_ids = torch.cat([img_ids, img_cond_seq_ids], dim=1) + + # The transformer forward() requires a guidance tensor even when guidance_embeds=False, + # because the Flux2TimestepGuidanceEmbeddings forward signature takes it unconditionally. + # All current Klein variants have guidance_embeds=False, so the value is ignored internally. + guidance_vec = torch.full((img.shape[0],), guidance, device=img.device, dtype=img.dtype) + + # Use scheduler if provided + use_scheduler = scheduler is not None + if use_scheduler: + # Set up scheduler with sigmas and mu for dynamic shifting + # Convert timesteps (0-1 range) to sigmas for the scheduler + # The scheduler will apply dynamic shifting internally using mu (if enabled in scheduler config) + sigmas = np.array(timesteps[:-1], dtype=np.float32) # Exclude final 0.0 + + # Check if scheduler supports sigmas parameter using inspect.signature + # FlowMatchHeunDiscreteScheduler and FlowMatchLCMScheduler don't support sigmas + set_timesteps_sig = inspect.signature(scheduler.set_timesteps) + supports_sigmas = "sigmas" in set_timesteps_sig.parameters + if supports_sigmas and mu is not None: + # Pass mu if provided - it will only be used if scheduler has use_dynamic_shifting=True + scheduler.set_timesteps(sigmas=sigmas.tolist(), mu=mu, device=img.device) + elif supports_sigmas: + scheduler.set_timesteps(sigmas=sigmas.tolist(), device=img.device) + else: + # Scheduler doesn't support sigmas (e.g., Heun, LCM) - use num_inference_steps + # + # Important for img2img callers: if the initial latent/noise blend was + # computed from a separate pre-scheduler schedule, that preblend may not + # match this scheduler's true first step exactly. + scheduler_kwargs: dict[str, Any] = {"num_inference_steps": len(sigmas), "device": img.device} + if mu is not None and "mu" in set_timesteps_sig.parameters: + scheduler_kwargs["mu"] = mu + scheduler.set_timesteps(**scheduler_kwargs) + num_scheduler_steps = len(scheduler.timesteps) + is_heun = hasattr(scheduler, "state_in_first_order") + user_step = 0 + + pbar = tqdm(total=total_steps, desc="Denoising") + for step_index in range(num_scheduler_steps): + timestep = scheduler.timesteps[step_index] + # Convert scheduler timestep (0-1000) to normalized (0-1) for the model + t_curr = timestep.item() / scheduler.config.num_train_timesteps + t_vec = torch.full((img.shape[0],), t_curr, dtype=img.dtype, device=img.device) + + # Track if we're in first or second order step (for Heun) + in_first_order = scheduler.state_in_first_order if is_heun else True + + # Run the transformer model (matching diffusers: guidance=guidance, return_dict=False) + output = model( + hidden_states=img, + encoder_hidden_states=txt, + timestep=t_vec, + img_ids=img_ids, + txt_ids=txt_ids, + guidance=guidance_vec, + return_dict=False, + ) + + # Extract the sample from the output (return_dict=False returns tuple) + pred = output[0] if isinstance(output, tuple) else output + + step_cfg_scale = cfg_scale[min(user_step, len(cfg_scale) - 1)] + + # Apply CFG if scale is not 1.0 + if not math.isclose(step_cfg_scale, 1.0): + if neg_txt is None: + raise ValueError("Negative text conditioning is required when cfg_scale is not 1.0.") + + neg_output = model( + hidden_states=img, + encoder_hidden_states=neg_txt, + timestep=t_vec, + img_ids=img_ids, + txt_ids=neg_txt_ids if neg_txt_ids is not None else txt_ids, + guidance=guidance_vec, + return_dict=False, + ) + + neg_pred = neg_output[0] if isinstance(neg_output, tuple) else neg_output + pred = neg_pred + step_cfg_scale * (pred - neg_pred) + + # Use scheduler.step() for the update + step_output = scheduler.step(model_output=pred, timestep=timestep, sample=img) + img = step_output.prev_sample + + # Get t_prev for inpainting (next sigma value) + if step_index + 1 < len(scheduler.sigmas): + t_prev = scheduler.sigmas[step_index + 1].item() + else: + t_prev = 0.0 + + # Apply inpainting merge at each step + if inpaint_extension is not None: + # Separate the generated latents from the reference conditioning + gen_img = img[:, :original_seq_len, :] + ref_img = img[:, original_seq_len:, :] + + # Merge only the generated part + gen_img = inpaint_extension.merge_intermediate_latents_with_init_latents(gen_img, t_prev) + + # Concatenate back together + img = torch.cat([gen_img, ref_img], dim=1) + + # For Heun, only increment user step after second-order step completes + if is_heun: + if not in_first_order: + user_step += 1 + if user_step <= total_steps: + pbar.update(1) + preview_img = img - t_curr * pred + if inpaint_extension is not None: + preview_img = inpaint_extension.merge_intermediate_latents_with_init_latents( + preview_img, 0.0 + ) + step_callback( + PipelineIntermediateState( + step=user_step, + order=2, + total_steps=total_steps, + timestep=int(t_curr * 1000), + latents=preview_img, + ), + ) + else: + user_step += 1 + if user_step <= total_steps: + pbar.update(1) + preview_img = img - t_curr * pred + if inpaint_extension is not None: + preview_img = inpaint_extension.merge_intermediate_latents_with_init_latents(preview_img, 0.0) + # Extract only the generated image portion for preview (exclude reference images) + callback_latents = preview_img[:, :original_seq_len, :] if img_cond_seq is not None else preview_img + step_callback( + PipelineIntermediateState( + step=user_step, + order=1, + total_steps=total_steps, + timestep=int(t_curr * 1000), + latents=callback_latents, + ), + ) + + pbar.close() + else: + # Manual Euler stepping (original behavior) + for step_index, (t_curr, t_prev) in tqdm(list(enumerate(zip(timesteps[:-1], timesteps[1:], strict=True)))): + t_vec = torch.full((img.shape[0],), t_curr, dtype=img.dtype, device=img.device) + + # Run the transformer model (matching diffusers: guidance=guidance, return_dict=False) + output = model( + hidden_states=img, + encoder_hidden_states=txt, + timestep=t_vec, + img_ids=img_ids, + txt_ids=txt_ids, + guidance=guidance_vec, + return_dict=False, + ) + + # Extract the sample from the output (return_dict=False returns tuple) + pred = output[0] if isinstance(output, tuple) else output + + step_cfg_scale = cfg_scale[step_index] + + # Apply CFG if scale is not 1.0 + if not math.isclose(step_cfg_scale, 1.0): + if neg_txt is None: + raise ValueError("Negative text conditioning is required when cfg_scale is not 1.0.") + + neg_output = model( + hidden_states=img, + encoder_hidden_states=neg_txt, + timestep=t_vec, + img_ids=img_ids, + txt_ids=neg_txt_ids if neg_txt_ids is not None else txt_ids, + guidance=guidance_vec, + return_dict=False, + ) + + neg_pred = neg_output[0] if isinstance(neg_output, tuple) else neg_output + pred = neg_pred + step_cfg_scale * (pred - neg_pred) + + # Euler step + preview_img = img - t_curr * pred + img = img + (t_prev - t_curr) * pred + + # Apply inpainting merge at each step + if inpaint_extension is not None: + # Separate the generated latents from the reference conditioning + gen_img = img[:, :original_seq_len, :] + ref_img = img[:, original_seq_len:, :] + + # Merge only the generated part + gen_img = inpaint_extension.merge_intermediate_latents_with_init_latents(gen_img, t_prev) + + # Concatenate back together + img = torch.cat([gen_img, ref_img], dim=1) + + # Handling preview images + preview_gen = preview_img[:, :original_seq_len, :] + preview_gen = inpaint_extension.merge_intermediate_latents_with_init_latents(preview_gen, 0.0) + + # Extract only the generated image portion for preview (exclude reference images) + callback_latents = preview_img[:, :original_seq_len, :] if img_cond_seq is not None else preview_img + step_callback( + PipelineIntermediateState( + step=step_index + 1, + order=1, + total_steps=total_steps, + timestep=int(t_curr), + latents=callback_latents, + ), + ) + + # Extract only the generated image portion (exclude concatenated reference images) + if img_cond_seq is not None: + img = img[:, :original_seq_len, :] + + return img diff --git a/invokeai/backend/flux2/ref_image_extension.py b/invokeai/backend/flux2/ref_image_extension.py new file mode 100644 index 00000000000..9cc6240db66 --- /dev/null +++ b/invokeai/backend/flux2/ref_image_extension.py @@ -0,0 +1,294 @@ +"""FLUX.2 Klein Reference Image Extension for multi-reference image editing. + +This module provides the Flux2RefImageExtension for FLUX.2 Klein models, +which handles encoding reference images using the FLUX.2 VAE and +generating the appropriate position IDs for multi-reference image editing. + +FLUX.2 Klein has built-in support for reference image editing (unlike FLUX.1 +which requires a separate Kontext model). +""" + +import math + +import torch +import torch.nn.functional as F +import torchvision.transforms as T +from einops import repeat +from PIL import Image + +from invokeai.app.invocations.fields import FluxKontextConditioningField +from invokeai.app.invocations.model import VAEField +from invokeai.app.services.shared.invocation_context import InvocationContext +from invokeai.backend.flux2.sampling_utils import pack_flux2 +from invokeai.backend.util.devices import TorchDevice + +# Maximum pixel counts for reference images (matches BFL FLUX.2 sampling.py) +# Single reference image: 2024² pixels, Multiple: 1024² pixels +MAX_PIXELS_SINGLE_REF = 2024**2 # ~4.1M pixels +MAX_PIXELS_MULTI_REF = 1024**2 # ~1M pixels + + +def resize_image_to_max_pixels(image: Image.Image, max_pixels: int) -> Image.Image: + """Resize image to fit within max_pixels while preserving aspect ratio. + + This matches the BFL FLUX.2 sampling.py cap_pixels() behavior. + + Args: + image: PIL Image to resize. + max_pixels: Maximum total pixel count (width * height). + + Returns: + Resized PIL Image (or original if already within bounds). + """ + width, height = image.size + pixel_count = width * height + + if pixel_count <= max_pixels: + return image + + # Calculate scale factor to fit within max_pixels (BFL approach) + scale = math.sqrt(max_pixels / pixel_count) + new_width = int(width * scale) + new_height = int(height * scale) + + # Ensure dimensions are at least 1 + new_width = max(1, new_width) + new_height = max(1, new_height) + + return image.resize((new_width, new_height), Image.Resampling.LANCZOS) + + +def generate_img_ids_flux2_with_offset( + latent_height: int, + latent_width: int, + batch_size: int, + device: torch.device, + idx_offset: int = 0, + h_offset: int = 0, + w_offset: int = 0, +) -> torch.Tensor: + """Generate tensor of image position ids with optional offsets for FLUX.2. + + FLUX.2 uses 4D position coordinates (T, H, W, L) for its rotary position embeddings. + Position IDs use int64 (long) dtype. + + Args: + latent_height: Height of image in latent space (before packing). + latent_width: Width of image in latent space (before packing). + batch_size: Number of images in the batch. + device: Device to create tensors on. + idx_offset: Offset for T (time/index) coordinate - use 1 for reference images. + h_offset: Spatial offset for H coordinate in latent space. + w_offset: Spatial offset for W coordinate in latent space. + + Returns: + Image position ids with shape [batch_size, (latent_height//2 * latent_width//2), 4]. + """ + # After packing, the spatial dimensions are halved due to the 2x2 patch structure + packed_height = latent_height // 2 + packed_width = latent_width // 2 + + # Convert spatial offsets from latent space to packed space + packed_h_offset = h_offset // 2 + packed_w_offset = w_offset // 2 + + # Create base tensor for position IDs with shape [packed_height, packed_width, 4] + # The 4 channels represent: [T, H, W, L] + img_ids = torch.zeros(packed_height, packed_width, 4, device=device, dtype=torch.long) + + # Set T (time/index offset) for all positions - use 1 for reference images + img_ids[..., 0] = idx_offset + + # Set H (height/y) coordinates with offset + h_coords = torch.arange(packed_height, device=device, dtype=torch.long) + packed_h_offset + img_ids[..., 1] = h_coords[:, None] + + # Set W (width/x) coordinates with offset + w_coords = torch.arange(packed_width, device=device, dtype=torch.long) + packed_w_offset + img_ids[..., 2] = w_coords[None, :] + + # L (layer) coordinate stays 0 + + # Expand to include batch dimension: [batch_size, (packed_height * packed_width), 4] + img_ids = img_ids.reshape(1, packed_height * packed_width, 4) + img_ids = repeat(img_ids, "1 s c -> b s c", b=batch_size) + + return img_ids + + +class Flux2RefImageExtension: + """Applies FLUX.2 Klein reference image conditioning. + + This extension handles encoding reference images using the FLUX.2 VAE + and generating the appropriate 4D position IDs for multi-reference image editing. + + FLUX.2 Klein has built-in support for reference image editing, unlike FLUX.1 + which requires a separate Kontext model. + """ + + def __init__( + self, + ref_image_conditioning: list[FluxKontextConditioningField], + context: InvocationContext, + vae_field: VAEField, + device: torch.device, + dtype: torch.dtype, + bn_mean: torch.Tensor | None = None, + bn_std: torch.Tensor | None = None, + ): + """Initialize the Flux2RefImageExtension. + + Args: + ref_image_conditioning: List of reference image conditioning fields. + context: The invocation context for loading models and images. + vae_field: The FLUX.2 VAE field for encoding images. + device: Target device for tensors. + dtype: Target dtype for tensors. + bn_mean: BN running mean for normalizing latents (shape: 128). + bn_std: BN running std for normalizing latents (shape: 128). + """ + self._context = context + self._device = device + self._dtype = dtype + self._vae_field = vae_field + self._bn_mean = bn_mean + self._bn_std = bn_std + self.ref_image_conditioning = ref_image_conditioning + + # Pre-process and cache the reference image latents and ids upon initialization + self.ref_image_latents, self.ref_image_ids = self._prepare_ref_images() + + def _bn_normalize(self, x: 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). + + Returns: + Normalized latents of same shape. + """ + assert self._bn_mean is not None and self._bn_std is not None + bn_mean = self._bn_mean.to(x.device, x.dtype) + bn_std = self._bn_std.to(x.device, x.dtype) + return (x - bn_mean) / bn_std + + def _prepare_ref_images(self) -> tuple[torch.Tensor, torch.Tensor]: + """Encode reference images and prepare their concatenated latents and IDs with spatial tiling.""" + all_latents = [] + all_ids = [] + + # Track cumulative dimensions for spatial tiling + canvas_h = 0 + canvas_w = 0 + + vae_info = self._context.models.load(self._vae_field.vae) + + # Determine max pixels based on number of reference images (BFL FLUX.2 approach) + num_refs = len(self.ref_image_conditioning) + max_pixels = MAX_PIXELS_SINGLE_REF if num_refs == 1 else MAX_PIXELS_MULTI_REF + + for idx, ref_image_field in enumerate(self.ref_image_conditioning): + image = self._context.images.get_pil(ref_image_field.image.image_name) + image = image.convert("RGB") + + # Resize large images to max pixel count (matches BFL FLUX.2 sampling.py) + image = resize_image_to_max_pixels(image, max_pixels) + + # Convert to tensor using torchvision transforms + transformation = T.Compose([T.ToTensor()]) + image_tensor = transformation(image) + # Convert from [0, 1] to [-1, 1] range expected by VAE + image_tensor = image_tensor * 2.0 - 1.0 + image_tensor = image_tensor.unsqueeze(0) # Add batch dimension + + # Encode using FLUX.2 VAE + with vae_info.model_on_device() as (_, vae): + vae_dtype = next(iter(vae.parameters())).dtype + image_tensor = image_tensor.to(device=TorchDevice.choose_torch_device(), dtype=vae_dtype) + + # FLUX.2 VAE uses diffusers API + latent_dist = vae.encode(image_tensor, return_dict=False)[0] + + # Use mode() for deterministic encoding (no sampling) + if hasattr(latent_dist, "mode"): + ref_image_latents_unpacked = latent_dist.mode() + elif hasattr(latent_dist, "sample"): + ref_image_latents_unpacked = latent_dist.sample() + else: + ref_image_latents_unpacked = latent_dist + + TorchDevice.empty_cache() + + # Extract tensor dimensions (B, 32, H, W for FLUX.2) + batch_size, _, latent_height, latent_width = ref_image_latents_unpacked.shape + + # Pad latents to be compatible with patch_size=2 + pad_h = (2 - latent_height % 2) % 2 + pad_w = (2 - latent_width % 2) % 2 + if pad_h > 0 or pad_w > 0: + ref_image_latents_unpacked = F.pad(ref_image_latents_unpacked, (0, pad_w, 0, pad_h), mode="circular") + _, _, latent_height, latent_width = ref_image_latents_unpacked.shape + + # Pack the latents using FLUX.2 pack function (32 channels -> 128) + ref_image_latents_packed = pack_flux2(ref_image_latents_unpacked).to(self._device, self._dtype) + + # Apply BN normalization to match the input latents scale + # This is critical - the transformer expects normalized latents + if self._bn_mean is not None and self._bn_std is not None: + ref_image_latents_packed = self._bn_normalize(ref_image_latents_packed) + + # Determine spatial offsets for this reference image + h_offset = 0 + w_offset = 0 + + if idx > 0: # First image starts at (0, 0) + # Calculate potential canvas dimensions for each tiling option + potential_h_vertical = canvas_h + latent_height + potential_w_horizontal = canvas_w + latent_width + + # Choose arrangement that minimizes the maximum dimension + if potential_h_vertical > potential_w_horizontal: + # Tile horizontally (to the right) + w_offset = canvas_w + canvas_w = canvas_w + latent_width + canvas_h = max(canvas_h, latent_height) + else: + # Tile vertically (below) + h_offset = canvas_h + canvas_h = canvas_h + latent_height + canvas_w = max(canvas_w, latent_width) + else: + canvas_h = latent_height + canvas_w = latent_width + + # Generate position IDs with 4D format (T, H, W, L) + # Use T-coordinate offset with scale=10 like diffusers Flux2Pipeline: + # T = scale + scale * idx (so first ref image is T=10, second is T=20, etc.) + # The generated image uses T=0, so this clearly separates reference images + t_offset = 10 + 10 * idx # scale=10 matches diffusers + ref_image_ids = generate_img_ids_flux2_with_offset( + latent_height=latent_height, + latent_width=latent_width, + batch_size=batch_size, + device=self._device, + idx_offset=t_offset, # Reference images use T=10, 20, 30... + h_offset=h_offset, + w_offset=w_offset, + ) + + all_latents.append(ref_image_latents_packed) + all_ids.append(ref_image_ids) + + # Concatenate all latents and IDs along the sequence dimension + concatenated_latents = torch.cat(all_latents, dim=1) + concatenated_ids = torch.cat(all_ids, dim=1) + + return concatenated_latents, concatenated_ids + + def ensure_batch_size(self, target_batch_size: int) -> None: + """Ensure the reference image latents and IDs match the target batch size.""" + if self.ref_image_latents.shape[0] != target_batch_size: + self.ref_image_latents = self.ref_image_latents.repeat(target_batch_size, 1, 1) + self.ref_image_ids = self.ref_image_ids.repeat(target_batch_size, 1, 1) diff --git a/invokeai/backend/flux2/sampling_utils.py b/invokeai/backend/flux2/sampling_utils.py new file mode 100644 index 00000000000..3981e912756 --- /dev/null +++ b/invokeai/backend/flux2/sampling_utils.py @@ -0,0 +1,206 @@ +"""FLUX.2 Klein Sampling Utilities. + +FLUX.2 Klein uses a 32-channel VAE (AutoencoderKLFlux2) instead of the 16-channel VAE +used by FLUX.1. This module provides sampling utilities adapted for FLUX.2. +""" + +import math + +import torch +from einops import rearrange + + +def get_noise_flux2( + num_samples: int, + height: int, + width: int, + device: torch.device, + dtype: torch.dtype, + seed: int, +) -> torch.Tensor: + """Generate noise for FLUX.2 Klein (32 channels). + + FLUX.2 uses a 32-channel VAE, so noise must have 32 channels. + The spatial dimensions are calculated to allow for packing. + + Args: + num_samples: Batch size. + height: Target image height in pixels. + width: Target image width in pixels. + device: Target device. + dtype: Target dtype. + seed: Random seed. + + Returns: + Noise tensor of shape (num_samples, 32, latent_h, latent_w). + """ + # We always generate noise on the same device and dtype then cast to ensure consistency. + rand_device = "cpu" + rand_dtype = torch.float16 + + # FLUX.2 uses 32 latent channels + # Latent dimensions: height/8, width/8 (from VAE downsampling) + # Must be divisible by 2 for packing (patchify step) + latent_h = 2 * math.ceil(height / 16) + latent_w = 2 * math.ceil(width / 16) + + return torch.randn( + num_samples, + 32, # FLUX.2 uses 32 latent channels (vs 16 for FLUX.1) + latent_h, + latent_w, + device=rand_device, + dtype=rand_dtype, + generator=torch.Generator(device=rand_device).manual_seed(seed), + ).to(device=device, dtype=dtype) + + +def pack_flux2(x: torch.Tensor) -> torch.Tensor: + """Pack latent image to flattened array of patch embeddings for FLUX.2. + + This performs the patchify + pack operation in one step: + 1. Patchify: Group 2x2 spatial patches into channels (C*4) + 2. Pack: Flatten spatial dimensions to sequence + + For 32-channel input: (B, 32, H, W) -> (B, H/2*W/2, 128) + + Args: + x: Latent tensor of shape (B, 32, H, W). + + Returns: + Packed tensor of shape (B, H/2*W/2, 128). + """ + # Same operation as FLUX.1 pack, but input has 32 channels -> output has 128 + return rearrange(x, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=2, pw=2) + + +def unpack_flux2(x: torch.Tensor, height: int, width: int) -> torch.Tensor: + """Unpack flat array of patch embeddings back to latent image for FLUX.2. + + This reverses the pack_flux2 operation: + 1. Unpack: Restore spatial dimensions from sequence + 2. Unpatchify: Restore 32 channels from 128 + + Args: + x: Packed tensor of shape (B, H/2*W/2, 128). + height: Target image height in pixels. + width: Target image width in pixels. + + Returns: + Latent tensor of shape (B, 32, H, W). + """ + # Calculate latent dimensions + latent_h = 2 * math.ceil(height / 16) + latent_w = 2 * math.ceil(width / 16) + + # Packed dimensions (after patchify) + packed_h = latent_h // 2 + packed_w = latent_w // 2 + + return rearrange( + x, + "b (h w) (c ph pw) -> b c (h ph) (w pw)", + h=packed_h, + w=packed_w, + ph=2, + pw=2, + ) + + +def compute_empirical_mu(image_seq_len: int, num_steps: int) -> float: + """Compute mu for FLUX.2 schedule shifting. + + Uses a fixed mu value of 2.02, matching ComfyUI's proven FLUX.2 configuration. + + The previous implementation (from diffusers' FLUX.1 pipeline) computed mu as a + linear function of image_seq_len, which produced excessively high values at + high resolutions (e.g., mu=3.23 at 2048x2048). This over-shifted the sigma + schedule, compressing almost all values above 0.9 and forcing the model to + denoise everything in the final 1-2 steps, causing severe grid/diamond artifacts. + + ComfyUI uses a fixed shift=2.02 for FLUX.2 Klein at all resolutions and produces + artifact-free images even at 2048x2048. + + Args: + image_seq_len: Number of image tokens (packed_h * packed_w). Currently unused. + num_steps: Number of denoising steps. Currently unused. + + Returns: + The mu value (fixed at 2.02). + """ + return 2.02 + + +def get_schedule_flux2( + num_steps: int, + image_seq_len: int, +) -> list[float]: + """Get linear timestep schedule for FLUX.2. + + Returns a linear sigma schedule from 1.0 to 1/num_steps. + The actual schedule shifting is handled by the FlowMatchEulerDiscreteScheduler + using the mu parameter and use_dynamic_shifting=True. + + Args: + num_steps: Number of denoising steps. + image_seq_len: Number of image tokens (packed_h * packed_w). Currently unused, + but kept for API compatibility. The scheduler computes shifting internally. + + Returns: + List of linear sigmas from 1.0 to 1/num_steps, plus final 0.0. + """ + import numpy as np + + # Create linear sigmas from 1.0 to 1/num_steps + # The scheduler will apply dynamic shifting using mu parameter + sigmas = np.linspace(1.0, 1 / num_steps, num_steps) + sigmas_list = [float(s) for s in sigmas] + + # Add final 0.0 for the last step (scheduler needs n+1 timesteps for n steps) + sigmas_list.append(0.0) + + return sigmas_list + + +def generate_img_ids_flux2(h: int, w: int, batch_size: int, device: torch.device) -> torch.Tensor: + """Generate tensor of image position ids for FLUX.2 with RoPE scaling. + + FLUX.2 uses 4D position coordinates (T, H, W, L) for its rotary position embeddings. + This is different from FLUX.1 which uses 3D coordinates. + + RoPE Scaling: For resolutions >1536x1536, position IDs are scaled down using + Position Interpolation to prevent RoPE degradation and diamond/grid artifacts. + + IMPORTANT: Position IDs must use int64 (long) dtype like diffusers, not bfloat16. + Using floating point dtype for position IDs can cause NaN in rotary embeddings. + + Args: + h: Height of image in latent space. + w: Width of image in latent space. + batch_size: Batch size. + device: Device. + + Returns: + Image position ids tensor of shape (batch_size, h/2*w/2, 4) with int64 dtype. + """ + # After packing, spatial dims are h/2 x w/2 + packed_h = h // 2 + packed_w = w // 2 + + # Create coordinate grids - 4D: (T, H, W, L) + # T = time/batch index, H = height, W = width, L = layer/channel + # Use int64 (long) dtype like diffusers + img_ids = torch.zeros(packed_h, packed_w, 4, device=device, dtype=torch.long) + + # T (time/batch) coordinate - set to 0 (already initialized) + # H coordinates + img_ids[..., 1] = torch.arange(packed_h, device=device, dtype=torch.long)[:, None] + # W coordinates + img_ids[..., 2] = torch.arange(packed_w, device=device, dtype=torch.long)[None, :] + # L (layer) coordinate - set to 0 (already initialized) + + # Flatten and expand for batch + img_ids = img_ids.reshape(1, packed_h * packed_w, 4) + img_ids = img_ids.expand(batch_size, -1, -1) + + return img_ids diff --git a/invokeai/backend/image_util/__init__.py b/invokeai/backend/image_util/__init__.py new file mode 100644 index 00000000000..bc5eed7ddd7 --- /dev/null +++ b/invokeai/backend/image_util/__init__.py @@ -0,0 +1,12 @@ +""" +Initialization file for invokeai.backend.image_util methods. +""" + +from invokeai.backend.image_util.infill_methods.patchmatch import PatchMatch # noqa: F401 +from invokeai.backend.image_util.pngwriter import ( # noqa: F401 + PngWriter, + PromptFormatter, + retrieve_metadata, + write_metadata, +) +from invokeai.backend.image_util.util import InitImageResizer, make_grid # noqa: F401 diff --git a/invokeai/backend/image_util/assets/CIELab_to_UPLab.icc b/invokeai/backend/image_util/assets/CIELab_to_UPLab.icc new file mode 100644 index 00000000000..3163cd3507d Binary files /dev/null and b/invokeai/backend/image_util/assets/CIELab_to_UPLab.icc differ diff --git a/invokeai/backend/image_util/basicsr/LICENSE b/invokeai/backend/image_util/basicsr/LICENSE new file mode 100644 index 00000000000..1c9b5b800ea --- /dev/null +++ b/invokeai/backend/image_util/basicsr/LICENSE @@ -0,0 +1,201 @@ + 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. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2022 BasicSR Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/invokeai/backend/image_util/basicsr/__init__.py b/invokeai/backend/image_util/basicsr/__init__.py new file mode 100644 index 00000000000..1d14b8e81e1 --- /dev/null +++ b/invokeai/backend/image_util/basicsr/__init__.py @@ -0,0 +1,18 @@ +""" +Adapted from https://github.com/XPixelGroup/BasicSR +License: Apache-2.0 + +As of Feb 2024, `basicsr` appears to be unmaintained. It imports a function from `torchvision` that is removed in +`torchvision` 0.17. Here is the deprecation warning: + + UserWarning: The torchvision.transforms.functional_tensor module is deprecated in 0.15 and will be **removed in + 0.17**. Please don't rely on it. You probably just need to use APIs in torchvision.transforms.functional or in + torchvision.transforms.v2.functional. + +As a result, a dependency on `basicsr` means we cannot keep our `torchvision` dependency up to date. + +Because we only rely on a single class `RRDBNet` from `basicsr`, we've copied the relevant code here and removed the +dependency on `basicsr`. + +The code is almost unchanged, only a few type annotations have been added. The license is also copied. +""" diff --git a/invokeai/backend/image_util/basicsr/arch_util.py b/invokeai/backend/image_util/basicsr/arch_util.py new file mode 100644 index 00000000000..45b3029ff87 --- /dev/null +++ b/invokeai/backend/image_util/basicsr/arch_util.py @@ -0,0 +1,75 @@ +from typing import Type + +import torch +from torch import nn as nn +from torch.nn import init as init +from torch.nn.modules.batchnorm import _BatchNorm + + +@torch.no_grad() +def default_init_weights( + module_list: list[nn.Module] | nn.Module, scale: float = 1, bias_fill: float = 0, **kwargs +) -> None: + """Initialize network weights. + + Args: + module_list (list[nn.Module] | nn.Module): Modules to be initialized. + scale (float): Scale initialized weights, especially for residual + blocks. Default: 1. + bias_fill (float): The value to fill bias. Default: 0 + kwargs (dict): Other arguments for initialization function. + """ + if not isinstance(module_list, list): + module_list = [module_list] + for module in module_list: + for m in module.modules(): + if isinstance(m, nn.Conv2d): + init.kaiming_normal_(m.weight, **kwargs) + m.weight.data *= scale + if m.bias is not None: + m.bias.data.fill_(bias_fill) + elif isinstance(m, nn.Linear): + init.kaiming_normal_(m.weight, **kwargs) + m.weight.data *= scale + if m.bias is not None: + m.bias.data.fill_(bias_fill) + elif isinstance(m, _BatchNorm): + init.constant_(m.weight, 1) + if m.bias is not None: + m.bias.data.fill_(bias_fill) + + +def make_layer(basic_block: Type[nn.Module], num_basic_block: int, **kwarg) -> nn.Sequential: + """Make layers by stacking the same blocks. + + Args: + basic_block (Type[nn.Module]): nn.Module class for basic block. + num_basic_block (int): number of blocks. + + Returns: + nn.Sequential: Stacked blocks in nn.Sequential. + """ + layers = [] + for _ in range(num_basic_block): + layers.append(basic_block(**kwarg)) + return nn.Sequential(*layers) + + +# TODO: may write a cpp file +def pixel_unshuffle(x: torch.Tensor, scale: int) -> torch.Tensor: + """Pixel unshuffle. + + Args: + x (Tensor): Input feature with shape (b, c, hh, hw). + scale (int): Downsample ratio. + + Returns: + Tensor: the pixel unshuffled feature. + """ + b, c, hh, hw = x.size() + out_channel = c * (scale**2) + assert hh % scale == 0 and hw % scale == 0 + h = hh // scale + w = hw // scale + x_view = x.view(b, c, h, scale, w, scale) + return x_view.permute(0, 1, 3, 5, 2, 4).reshape(b, out_channel, h, w) diff --git a/invokeai/backend/image_util/basicsr/rrdbnet_arch.py b/invokeai/backend/image_util/basicsr/rrdbnet_arch.py new file mode 100644 index 00000000000..a99a6971236 --- /dev/null +++ b/invokeai/backend/image_util/basicsr/rrdbnet_arch.py @@ -0,0 +1,125 @@ +import torch +from torch import nn as nn +from torch.nn import functional as F + +from invokeai.backend.image_util.basicsr.arch_util import default_init_weights, make_layer, pixel_unshuffle + + +class ResidualDenseBlock(nn.Module): + """Residual Dense Block. + + Used in RRDB block in ESRGAN. + + Args: + num_feat (int): Channel number of intermediate features. + num_grow_ch (int): Channels for each growth. + """ + + def __init__(self, num_feat: int = 64, num_grow_ch: int = 32) -> None: + super(ResidualDenseBlock, self).__init__() + self.conv1 = nn.Conv2d(num_feat, num_grow_ch, 3, 1, 1) + self.conv2 = nn.Conv2d(num_feat + num_grow_ch, num_grow_ch, 3, 1, 1) + self.conv3 = nn.Conv2d(num_feat + 2 * num_grow_ch, num_grow_ch, 3, 1, 1) + self.conv4 = nn.Conv2d(num_feat + 3 * num_grow_ch, num_grow_ch, 3, 1, 1) + self.conv5 = nn.Conv2d(num_feat + 4 * num_grow_ch, num_feat, 3, 1, 1) + + self.lrelu = nn.LeakyReLU(negative_slope=0.2, inplace=True) + + # initialization + default_init_weights([self.conv1, self.conv2, self.conv3, self.conv4, self.conv5], 0.1) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x1 = self.lrelu(self.conv1(x)) + x2 = self.lrelu(self.conv2(torch.cat((x, x1), 1))) + x3 = self.lrelu(self.conv3(torch.cat((x, x1, x2), 1))) + x4 = self.lrelu(self.conv4(torch.cat((x, x1, x2, x3), 1))) + x5 = self.conv5(torch.cat((x, x1, x2, x3, x4), 1)) + # Empirically, we use 0.2 to scale the residual for better performance + return x5 * 0.2 + x + + +class RRDB(nn.Module): + """Residual in Residual Dense Block. + + Used in RRDB-Net in ESRGAN. + + Args: + num_feat (int): Channel number of intermediate features. + num_grow_ch (int): Channels for each growth. + """ + + def __init__(self, num_feat: int, num_grow_ch: int = 32) -> None: + super(RRDB, self).__init__() + self.rdb1 = ResidualDenseBlock(num_feat, num_grow_ch) + self.rdb2 = ResidualDenseBlock(num_feat, num_grow_ch) + self.rdb3 = ResidualDenseBlock(num_feat, num_grow_ch) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + out = self.rdb1(x) + out = self.rdb2(out) + out = self.rdb3(out) + # Empirically, we use 0.2 to scale the residual for better performance + return out * 0.2 + x + + +class RRDBNet(nn.Module): + """Networks consisting of Residual in Residual Dense Block, which is used + in ESRGAN. + + ESRGAN: Enhanced Super-Resolution Generative Adversarial Networks. + + We extend ESRGAN for scale x2 and scale x1. + Note: This is one option for scale 1, scale 2 in RRDBNet. + We first employ the pixel-unshuffle (an inverse operation of pixelshuffle to reduce the spatial size + and enlarge the channel size before feeding inputs into the main ESRGAN architecture. + + Args: + num_in_ch (int): Channel number of inputs. + num_out_ch (int): Channel number of outputs. + num_feat (int): Channel number of intermediate features. + Default: 64 + num_block (int): Block number in the trunk network. Defaults: 23 + num_grow_ch (int): Channels for each growth. Default: 32. + """ + + def __init__( + self, + num_in_ch: int, + num_out_ch: int, + scale: int = 4, + num_feat: int = 64, + num_block: int = 23, + num_grow_ch: int = 32, + ) -> None: + super(RRDBNet, self).__init__() + self.scale = scale + if scale == 2: + num_in_ch = num_in_ch * 4 + elif scale == 1: + num_in_ch = num_in_ch * 16 + self.conv_first = nn.Conv2d(num_in_ch, num_feat, 3, 1, 1) + self.body = make_layer(RRDB, num_block, num_feat=num_feat, num_grow_ch=num_grow_ch) + self.conv_body = nn.Conv2d(num_feat, num_feat, 3, 1, 1) + # upsample + self.conv_up1 = nn.Conv2d(num_feat, num_feat, 3, 1, 1) + self.conv_up2 = nn.Conv2d(num_feat, num_feat, 3, 1, 1) + self.conv_hr = nn.Conv2d(num_feat, num_feat, 3, 1, 1) + self.conv_last = nn.Conv2d(num_feat, num_out_ch, 3, 1, 1) + + self.lrelu = nn.LeakyReLU(negative_slope=0.2, inplace=True) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if self.scale == 2: + feat = pixel_unshuffle(x, scale=2) + elif self.scale == 1: + feat = pixel_unshuffle(x, scale=4) + else: + feat = x + feat = self.conv_first(feat) + body_feat = self.conv_body(self.body(feat)) + feat = feat + body_feat + # upsample + feat = self.lrelu(self.conv_up1(F.interpolate(feat, scale_factor=2, mode="nearest"))) + feat = self.lrelu(self.conv_up2(F.interpolate(feat, scale_factor=2, mode="nearest"))) + out = self.conv_last(self.lrelu(self.conv_hr(feat))) + return out diff --git a/invokeai/backend/image_util/canny.py b/invokeai/backend/image_util/canny.py new file mode 100644 index 00000000000..c1628dc1828 --- /dev/null +++ b/invokeai/backend/image_util/canny.py @@ -0,0 +1,41 @@ +import cv2 +from PIL import Image + +from invokeai.backend.image_util.util import ( + cv2_to_pil, + normalize_image_channel_count, + pil_to_cv2, + resize_image_to_resolution, +) + + +def get_canny_edges( + image: Image.Image, low_threshold: int, high_threshold: int, detect_resolution: int, image_resolution: int +) -> Image.Image: + """Returns the edges of an image using the Canny edge detection algorithm. + + Adapted from https://github.com/huggingface/controlnet_aux (Apache-2.0 license). + + Args: + image: The input image. + low_threshold: The lower threshold for the hysteresis procedure. + high_threshold: The upper threshold for the hysteresis procedure. + input_resolution: The resolution of the input image. The image will be resized to this resolution before edge detection. + output_resolution: The resolution of the output image. The edges will be resized to this resolution before returning. + + Returns: + The Canny edges of the input image. + """ + + if image.mode != "RGB": + image = image.convert("RGB") + + np_image = pil_to_cv2(image) + np_image = normalize_image_channel_count(np_image) + np_image = resize_image_to_resolution(np_image, detect_resolution) + + edge_map = cv2.Canny(np_image, low_threshold, high_threshold) + edge_map = normalize_image_channel_count(edge_map) + edge_map = resize_image_to_resolution(edge_map, image_resolution) + + return cv2_to_pil(edge_map) diff --git a/invokeai/backend/image_util/color_conversion.py b/invokeai/backend/image_util/color_conversion.py new file mode 100644 index 00000000000..0dc368f9835 --- /dev/null +++ b/invokeai/backend/image_util/color_conversion.py @@ -0,0 +1,1084 @@ +from math import pi as PI + +import torch + +MAX_FLOAT = torch.finfo(torch.tensor(1.0).dtype).max +_SRGB_TO_LINEAR_THRESHOLD = 0.0404482362771082 +_LINEAR_TO_SRGB_THRESHOLD = 0.0031308 +_SRGB_TO_XYZ_D65_MATRIX = ( + (0.4124, 0.3576, 0.1805), + (0.2126, 0.7152, 0.0722), + (0.0193, 0.1192, 0.9505), +) +_XYZ_D65_TO_SRGB_MATRIX = ( + (3.2406255, -1.5372080, -0.4986286), + (-0.9689307, 1.8757561, 0.0415175), + (0.0557101, -0.2040211, 1.0569959), +) +_BRADFORD_MATRIX = ( + (0.8951, 0.2664, -0.1614), + (-0.7502, 1.7135, 0.0367), + (0.0389, -0.0685, 1.0296), +) +_BRADFORD_INVERSE_MATRIX = ( + (0.9869929, -0.1470543, 0.1599627), + (0.4323053, 0.5183603, 0.0492912), + (-0.0085287, 0.0400428, 0.9684867), +) +_REFERENCE_ILLUMINANTS = { + "D65": (0.950489, 1.0, 1.088840), + "D50": (0.964212, 1.0, 0.825188), +} +_LINEAR_SRGB_TO_OKLAB_LMS_MATRIX = ( + (0.4122214708, 0.5363325363, 0.0514459929), + (0.2119034982, 0.6806995451, 0.1073969566), + (0.0883024619, 0.2817188376, 0.6299787005), +) +_LMS_CUBE_ROOT_TO_OKLAB_MATRIX = ( + (0.2104542553, 0.7936177850, -0.0040720468), + (1.9779984951, -2.4285922050, 0.4505937099), + (0.0259040371, 0.7827717662, -0.8086757660), +) +_OKLAB_TO_LMS_CUBE_ROOT_MATRIX = ( + (1.0, 0.3963377774, 0.2158037573), + (1.0, -0.1055613458, -0.0638541728), + (1.0, -0.0894841775, -1.2914855480), +) +_LMS_TO_LINEAR_SRGB_MATRIX = ( + (4.0767416621, -3.3077115913, 0.2309699292), + (-1.2684380046, 2.6097574011, -0.3413193965), + (-0.0041960863, -0.7034186147, 1.7076147010), +) + + +def _require_color_tensor(color_tensor: torch.Tensor) -> torch.Tensor: + if color_tensor.ndim != 3 or color_tensor.shape[0] != 3: + raise ValueError("color_tensor must be a 3xHxW tensor") + return color_tensor + + +def _require_reference_illuminant(reference_illuminant: str) -> str: + normalized = reference_illuminant.upper() + if normalized not in _REFERENCE_ILLUMINANTS: + raise ValueError(f"Unsupported reference_illuminant: {reference_illuminant}") + return normalized + + +def _full_like_spatial(reference_tensor: torch.Tensor, fill_value: float) -> torch.Tensor: + return torch.full( + reference_tensor.shape[1:], fill_value, dtype=reference_tensor.dtype, device=reference_tensor.device + ) + + +def _degrees_from_unit_hue(unit_hue_tensor: torch.Tensor) -> torch.Tensor: + return torch.remainder(unit_hue_tensor * 360.0, 360.0) + + +def _unit_hue_from_degrees(hue_tensor: torch.Tensor) -> torch.Tensor: + return torch.remainder(hue_tensor, 360.0) / 360.0 + + +def _matrix_tensor(matrix: tuple[tuple[float, ...], ...], reference_tensor: torch.Tensor) -> torch.Tensor: + return torch.tensor(matrix, dtype=reference_tensor.dtype, device=reference_tensor.device) + + +def _apply_matrix(matrix: tuple[tuple[float, ...], ...], color_tensor: torch.Tensor) -> torch.Tensor: + return torch.einsum("rc,cwh->rwh", _matrix_tensor(matrix, color_tensor), color_tensor) + + +def _reference_white_tensor(reference_illuminant: str, reference_tensor: torch.Tensor) -> torch.Tensor: + return torch.tensor( + _REFERENCE_ILLUMINANTS[reference_illuminant], dtype=reference_tensor.dtype, device=reference_tensor.device + ).view(3, 1, 1) + + +def _adapt_xyz(xyz_tensor: torch.Tensor, source_illuminant: str, target_illuminant: str) -> torch.Tensor: + xyz_tensor = _require_color_tensor(xyz_tensor) + source_illuminant = _require_reference_illuminant(source_illuminant) + target_illuminant = _require_reference_illuminant(target_illuminant) + if source_illuminant == target_illuminant: + return xyz_tensor + source_white_lms = _apply_matrix(_BRADFORD_MATRIX, _reference_white_tensor(source_illuminant, xyz_tensor)) + target_white_lms = _apply_matrix(_BRADFORD_MATRIX, _reference_white_tensor(target_illuminant, xyz_tensor)) + xyz_lms = _apply_matrix(_BRADFORD_MATRIX, xyz_tensor) + adapted_lms = xyz_lms * (target_white_lms / source_white_lms) + return _apply_matrix(_BRADFORD_INVERSE_MATRIX, adapted_lms) + + +def srgb_from_linear_srgb(linear_srgb_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW linear-light sRGB tensor in [0, 1] to gamma-corrected sRGB.""" + + linear_srgb_tensor = _require_color_tensor(linear_srgb_tensor) + linear_srgb_tensor = linear_srgb_tensor.clamp(0.0, 1.0) + return torch.where( + linear_srgb_tensor <= _LINEAR_TO_SRGB_THRESHOLD, + linear_srgb_tensor * 12.92, + 1.055 * torch.pow(linear_srgb_tensor, 1.0 / 2.4) - 0.055, + ) + + +def linear_srgb_from_srgb(srgb_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW gamma-corrected sRGB tensor in [0, 1] to linear-light sRGB.""" + + srgb_tensor = _require_color_tensor(srgb_tensor) + return torch.where( + srgb_tensor <= _SRGB_TO_LINEAR_THRESHOLD, + srgb_tensor / 12.92, + torch.pow((srgb_tensor + 0.055) / 1.055, 2.4), + ) + + +def xyz_from_linear_srgb(linear_srgb_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW linear-light sRGB tensor to normalized XYZ, where D65 white is approximately 1.0.""" + + linear_srgb_tensor = _require_color_tensor(linear_srgb_tensor) + return _apply_matrix(_SRGB_TO_XYZ_D65_MATRIX, linear_srgb_tensor) + + +def linear_srgb_from_xyz(xyz_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW normalized XYZ tensor, where D65 white is approximately 1.0, to linear-light sRGB.""" + + xyz_tensor = _require_color_tensor(xyz_tensor) + return _apply_matrix(_XYZ_D65_TO_SRGB_MATRIX, xyz_tensor) + + +def xyz_from_srgb(srgb_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW gamma-corrected sRGB tensor to normalized XYZ, where D65 white is approximately 1.0.""" + + srgb_tensor = _require_color_tensor(srgb_tensor) + return xyz_from_linear_srgb(linear_srgb_from_srgb(srgb_tensor)) + + +def srgb_from_xyz(xyz_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW normalized XYZ tensor, where D65 white is approximately 1.0, to gamma-corrected sRGB.""" + + xyz_tensor = _require_color_tensor(xyz_tensor) + return srgb_from_linear_srgb(linear_srgb_from_xyz(xyz_tensor)) + + +def xyz_d65_to_d50(xyz_tensor: torch.Tensor) -> torch.Tensor: + """Adapt a 3xHxW normalized XYZ tensor from D65 to D50 using Bradford chromatic adaptation.""" + + return _adapt_xyz(xyz_tensor, source_illuminant="D65", target_illuminant="D50") + + +def xyz_d50_to_d65(xyz_tensor: torch.Tensor) -> torch.Tensor: + """Adapt a 3xHxW normalized XYZ tensor from D50 to D65 using Bradford chromatic adaptation.""" + + return _adapt_xyz(xyz_tensor, source_illuminant="D50", target_illuminant="D65") + + +def _lab_from_xyz_helper(channel_illuminant_quotient_tensor: torch.Tensor) -> torch.Tensor: + delta = 6.0 / 29.0 + return torch.where( + torch.gt(channel_illuminant_quotient_tensor, delta**3.0), + torch.pow(channel_illuminant_quotient_tensor, 1.0 / 3.0), + torch.add(torch.div(channel_illuminant_quotient_tensor, 3.0 * (delta**2.0)), 4.0 / 29.0), + ) + + +def _xyz_from_lab_helper(channel_tensor: torch.Tensor) -> torch.Tensor: + delta = 6.0 / 29.0 + return torch.where( + torch.gt(channel_tensor, delta), + torch.pow(channel_tensor, 3.0), + torch.mul(3.0 * (delta**2.0), torch.sub(channel_tensor, 4.0 / 29.0)), + ) + + +def lab_from_xyz(xyz_tensor: torch.Tensor, reference_illuminant: str = "D65") -> torch.Tensor: + """Convert a 3xHxW normalized XYZ tensor to CIELAB using the given reference illuminant.""" + + xyz_tensor = _require_color_tensor(xyz_tensor) + reference_illuminant = _require_reference_illuminant(reference_illuminant) + illuminant = _reference_white_tensor(reference_illuminant, xyz_tensor) + l_tensor = torch.sub(torch.mul(_lab_from_xyz_helper(torch.div(xyz_tensor[1, :, :], illuminant[1])), 116.0), 16.0) + a_tensor = torch.mul( + torch.sub( + _lab_from_xyz_helper(torch.div(xyz_tensor[0, :, :], illuminant[0])), + _lab_from_xyz_helper(torch.div(xyz_tensor[1, :, :], illuminant[1])), + ), + 500.0, + ) + b_tensor = torch.mul( + torch.sub( + _lab_from_xyz_helper(torch.div(xyz_tensor[1, :, :], illuminant[1])), + _lab_from_xyz_helper(torch.div(xyz_tensor[2, :, :], illuminant[2])), + ), + 200.0, + ) + return torch.stack([l_tensor, a_tensor, b_tensor]) + + +def xyz_from_lab(lab_tensor: torch.Tensor, reference_illuminant: str = "D65") -> torch.Tensor: + """Convert a 3xHxW CIELAB tensor to normalized XYZ using the given reference illuminant.""" + + lab_tensor = _require_color_tensor(lab_tensor) + reference_illuminant = _require_reference_illuminant(reference_illuminant) + illuminant = _reference_white_tensor(reference_illuminant, lab_tensor) + fy_tensor = (lab_tensor[0, :, :] + 16.0) / 116.0 + fx_tensor = fy_tensor + (lab_tensor[1, :, :] / 500.0) + fz_tensor = fy_tensor - (lab_tensor[2, :, :] / 200.0) + return torch.stack( + [ + illuminant[0] * _xyz_from_lab_helper(fx_tensor), + illuminant[1] * _xyz_from_lab_helper(fy_tensor), + illuminant[2] * _xyz_from_lab_helper(fz_tensor), + ] + ) + + +def lab_from_linear_srgb(linear_srgb_tensor: torch.Tensor, reference_illuminant: str = "D65") -> torch.Tensor: + """Convert a 3xHxW linear-light sRGB tensor to CIELAB using the given reference illuminant.""" + + linear_srgb_tensor = _require_color_tensor(linear_srgb_tensor) + reference_illuminant = _require_reference_illuminant(reference_illuminant) + xyz_tensor = xyz_from_linear_srgb(linear_srgb_tensor) + if reference_illuminant != "D65": + xyz_tensor = _adapt_xyz(xyz_tensor, source_illuminant="D65", target_illuminant=reference_illuminant) + return lab_from_xyz(xyz_tensor, reference_illuminant=reference_illuminant) + + +def linear_srgb_from_lab(lab_tensor: torch.Tensor, reference_illuminant: str = "D65") -> torch.Tensor: + """Convert a 3xHxW CIELAB tensor to linear-light sRGB using the given reference illuminant.""" + + lab_tensor = _require_color_tensor(lab_tensor) + reference_illuminant = _require_reference_illuminant(reference_illuminant) + xyz_tensor = xyz_from_lab(lab_tensor, reference_illuminant=reference_illuminant) + if reference_illuminant != "D65": + xyz_tensor = _adapt_xyz(xyz_tensor, source_illuminant=reference_illuminant, target_illuminant="D65") + return linear_srgb_from_xyz(xyz_tensor) + + +def lab_from_srgb(srgb_tensor: torch.Tensor, reference_illuminant: str = "D65") -> torch.Tensor: + """Convert a 3xHxW gamma-corrected sRGB tensor to CIELAB using the given reference illuminant.""" + + srgb_tensor = _require_color_tensor(srgb_tensor) + return lab_from_linear_srgb(linear_srgb_from_srgb(srgb_tensor), reference_illuminant=reference_illuminant) + + +def srgb_from_lab(lab_tensor: torch.Tensor, reference_illuminant: str = "D65") -> torch.Tensor: + """Convert a 3xHxW CIELAB tensor to gamma-corrected sRGB using the given reference illuminant.""" + + lab_tensor = _require_color_tensor(lab_tensor) + return srgb_from_linear_srgb(linear_srgb_from_lab(lab_tensor, reference_illuminant=reference_illuminant)) + + +def oklab_from_linear_srgb(linear_srgb_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW linear-light sRGB tensor to Oklab.""" + + linear_srgb_tensor = _require_color_tensor(linear_srgb_tensor) + lms_tensor = _apply_matrix(_LINEAR_SRGB_TO_OKLAB_LMS_MATRIX, linear_srgb_tensor) + lms_cbrt_tensor = torch.sign(lms_tensor) * torch.pow(torch.abs(lms_tensor), 1.0 / 3.0) + return _apply_matrix(_LMS_CUBE_ROOT_TO_OKLAB_MATRIX, lms_cbrt_tensor) + + +def linear_srgb_from_oklab(oklab_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW Oklab tensor to linear-light sRGB.""" + + oklab_tensor = _require_color_tensor(oklab_tensor) + lms_cbrt_tensor = _apply_matrix(_OKLAB_TO_LMS_CUBE_ROOT_MATRIX, oklab_tensor) + lms_tensor = lms_cbrt_tensor**3 + return _apply_matrix(_LMS_TO_LINEAR_SRGB_MATRIX, lms_tensor) + + +def oklab_from_srgb(srgb_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW gamma-corrected sRGB tensor to Oklab.""" + + srgb_tensor = _require_color_tensor(srgb_tensor) + return oklab_from_linear_srgb(linear_srgb_from_srgb(srgb_tensor)) + + +def srgb_from_oklab(oklab_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW Oklab tensor to gamma-corrected sRGB.""" + + oklab_tensor = _require_color_tensor(oklab_tensor) + return srgb_from_linear_srgb(linear_srgb_from_oklab(oklab_tensor)) + + +def oklab_from_xyz(xyz_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW normalized XYZ tensor, where D65 white is approximately 1.0, to Oklab.""" + + xyz_tensor = _require_color_tensor(xyz_tensor) + return oklab_from_linear_srgb(linear_srgb_from_xyz(xyz_tensor)) + + +def xyz_from_oklab(oklab_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW Oklab tensor to normalized XYZ, where D65 white is approximately 1.0.""" + + oklab_tensor = _require_color_tensor(oklab_tensor) + return xyz_from_linear_srgb(linear_srgb_from_oklab(oklab_tensor)) + + +def oklch_from_oklab(oklab_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW Oklab tensor to Oklch, with hue in degrees.""" + + oklab_tensor = _require_color_tensor(oklab_tensor) + lightness = oklab_tensor[0, ...] + chroma = torch.sqrt(oklab_tensor[1, ...] ** 2 + oklab_tensor[2, ...] ** 2) + hue = torch.remainder(torch.rad2deg(torch.atan2(oklab_tensor[2, ...], oklab_tensor[1, ...])), 360.0) + return torch.stack([lightness, chroma, hue]) + + +def oklab_from_oklch(oklch_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW Oklch tensor, with hue in degrees, to Oklab.""" + + oklch_tensor = _require_color_tensor(oklch_tensor) + hue_radians = torch.deg2rad(oklch_tensor[2, ...]) + a_channel = oklch_tensor[1, ...] * torch.cos(hue_radians) + b_channel = oklch_tensor[1, ...] * torch.sin(hue_radians) + return torch.stack([oklch_tensor[0, ...], a_channel, b_channel]) + + +def linear_srgb_from_oklch(oklch_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW Oklch tensor directly to linear-light sRGB.""" + + oklch_tensor = _require_color_tensor(oklch_tensor) + return linear_srgb_from_oklab(oklab_from_oklch(oklch_tensor)) + + +def oklch_from_linear_srgb(linear_srgb_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW linear-light sRGB tensor directly to Oklch.""" + + linear_srgb_tensor = _require_color_tensor(linear_srgb_tensor) + return oklch_from_oklab(oklab_from_linear_srgb(linear_srgb_tensor)) + + +def oklch_from_srgb(srgb_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW gamma-corrected sRGB tensor directly to Oklch.""" + + srgb_tensor = _require_color_tensor(srgb_tensor) + return oklch_from_linear_srgb(linear_srgb_from_srgb(srgb_tensor)) + + +def srgb_from_oklch(oklch_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW Oklch tensor directly to gamma-corrected sRGB.""" + + oklch_tensor = _require_color_tensor(oklch_tensor) + return srgb_from_linear_srgb(linear_srgb_from_oklch(oklch_tensor)) + + +def oklch_from_xyz(xyz_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW normalized XYZ tensor, where D65 white is approximately 1.0, to Oklch.""" + + xyz_tensor = _require_color_tensor(xyz_tensor) + return oklch_from_oklab(oklab_from_xyz(xyz_tensor)) + + +def xyz_from_oklch(oklch_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW Oklch tensor to normalized XYZ, where D65 white is approximately 1.0.""" + + oklch_tensor = _require_color_tensor(oklch_tensor) + return xyz_from_oklab(oklab_from_oklch(oklch_tensor)) + + +def _max_srgb_saturation_tensor(units_ab_tensor: torch.Tensor, steps: int = 1) -> torch.Tensor: + rgb_k_matrix = torch.tensor( + [ + [1.19086277, 1.76576728, 0.59662641, 0.75515197, 0.56771245], + [0.73956515, -0.45954494, 0.08285427, 0.12541070, 0.14503204], + [1.35733652, -0.00915799, -1.15130210, -0.50559606, 0.00692167], + ], + dtype=units_ab_tensor.dtype, + device=units_ab_tensor.device, + ) + rgb_w_matrix = _matrix_tensor(_LMS_TO_LINEAR_SRGB_MATRIX, units_ab_tensor) + cond_r_tensor = torch.add( + torch.mul(-1.88170328, units_ab_tensor[0, :, :]), torch.mul(-0.80936493, units_ab_tensor[1, :, :]) + ) + cond_g_tensor = torch.add( + torch.mul(1.81444104, units_ab_tensor[0, :, :]), torch.mul(-1.19445276, units_ab_tensor[1, :, :]) + ) + terms_tensor = torch.stack( + [ + torch.ones(units_ab_tensor.shape[1:], dtype=units_ab_tensor.dtype, device=units_ab_tensor.device), + units_ab_tensor[0, :, :], + units_ab_tensor[1, :, :], + torch.pow(units_ab_tensor[0, :, :], 2.0), + torch.mul(units_ab_tensor[0, :, :], units_ab_tensor[1, :, :]), + ] + ) + s_tensor = torch.where( + torch.gt(cond_r_tensor, 1.0), + torch.einsum("twh, t -> wh", terms_tensor, rgb_k_matrix[0]), + torch.where( + torch.gt(cond_g_tensor, 1.0), + torch.einsum("twh, t -> wh", terms_tensor, rgb_k_matrix[1]), + torch.einsum("twh, t -> wh", terms_tensor, rgb_k_matrix[2]), + ), + ) + k_lms_matrix = _matrix_tensor(tuple(row[1:] for row in _OKLAB_TO_LMS_CUBE_ROOT_MATRIX), units_ab_tensor) + k_lms_tensor = torch.einsum("tc, cwh -> twh", k_lms_matrix, units_ab_tensor) + for _ in range(steps): + root_lms_tensor = torch.add(torch.mul(k_lms_tensor, s_tensor), 1.0) + lms_tensor = torch.pow(root_lms_tensor, 3.0) + lms_ds_tensor = torch.mul(torch.mul(k_lms_tensor, torch.pow(root_lms_tensor, 2.0)), 3.0) + lms_ds2_tensor = torch.mul(torch.mul(torch.pow(k_lms_tensor, 2.0), root_lms_tensor), 6.0) + f_tensor = torch.where( + torch.gt(cond_r_tensor, 1.0), + torch.einsum("c, cwh -> wh", rgb_w_matrix[0], lms_tensor), + torch.where( + torch.gt(cond_g_tensor, 1.0), + torch.einsum("c, cwh -> wh", rgb_w_matrix[1], lms_tensor), + torch.einsum("c, cwh -> wh", rgb_w_matrix[2], lms_tensor), + ), + ) + f_tensor_1 = torch.where( + torch.gt(cond_r_tensor, 1.0), + torch.einsum("c, cwh -> wh", rgb_w_matrix[0], lms_ds_tensor), + torch.where( + torch.gt(cond_g_tensor, 1.0), + torch.einsum("c, cwh -> wh", rgb_w_matrix[1], lms_ds_tensor), + torch.einsum("c, cwh -> wh", rgb_w_matrix[2], lms_ds_tensor), + ), + ) + f_tensor_2 = torch.where( + torch.gt(cond_r_tensor, 1.0), + torch.einsum("c, cwh -> wh", rgb_w_matrix[0], lms_ds2_tensor), + torch.where( + torch.gt(cond_g_tensor, 1.0), + torch.einsum("c, cwh -> wh", rgb_w_matrix[1], lms_ds2_tensor), + torch.einsum("c, cwh -> wh", rgb_w_matrix[2], lms_ds2_tensor), + ), + ) + s_tensor = torch.sub( + s_tensor, + torch.div( + torch.mul(f_tensor, f_tensor_1), + torch.sub(torch.pow(f_tensor_1, 2.0), torch.mul(torch.mul(f_tensor, f_tensor_2), 0.5)), + ), + ) + return s_tensor + + +def _find_cusp_tensor(units_ab_tensor: torch.Tensor, steps: int = 1) -> torch.Tensor: + s_cusp_tensor = _max_srgb_saturation_tensor(units_ab_tensor, steps=steps) + oklab_tensor = torch.stack( + [ + torch.ones(s_cusp_tensor.shape, dtype=s_cusp_tensor.dtype, device=s_cusp_tensor.device), + torch.mul(s_cusp_tensor, units_ab_tensor[0, :, :]), + torch.mul(s_cusp_tensor, units_ab_tensor[1, :, :]), + ] + ) + rgb_at_max_tensor = linear_srgb_from_oklab(oklab_tensor) + l_cusp_tensor = torch.pow(torch.div(1.0, rgb_at_max_tensor.max(0).values), 1.0 / 3.0) + c_cusp_tensor = torch.mul(l_cusp_tensor, s_cusp_tensor) + return torch.stack([l_cusp_tensor, c_cusp_tensor]) + + +def _find_gamut_intersection_tensor( + units_ab_tensor: torch.Tensor, + l_1_tensor: torch.Tensor, + c_1_tensor: torch.Tensor, + l_0_tensor: torch.Tensor, + steps: int = 1, + steps_outer: int = 1, + lc_cusps_tensor: torch.Tensor | None = None, +) -> torch.Tensor: + if lc_cusps_tensor is None: + lc_cusps_tensor = _find_cusp_tensor(units_ab_tensor, steps=steps) + cond_tensor = torch.sub( + torch.mul(torch.sub(l_1_tensor, l_0_tensor), lc_cusps_tensor[1, :, :]), + torch.mul(torch.sub(lc_cusps_tensor[0, :, :], l_0_tensor), c_1_tensor), + ) + t_tensor = torch.where( + torch.le(cond_tensor, 0.0), + torch.div( + torch.mul(lc_cusps_tensor[1, :, :], l_0_tensor), + torch.add( + torch.mul(c_1_tensor, lc_cusps_tensor[0, :, :]), + torch.mul(lc_cusps_tensor[1, :, :], torch.sub(l_0_tensor, l_1_tensor)), + ), + ), + torch.div( + torch.mul(lc_cusps_tensor[1, :, :], torch.sub(l_0_tensor, 1.0)), + torch.add( + torch.mul(c_1_tensor, torch.sub(lc_cusps_tensor[0, :, :], 1.0)), + torch.mul(lc_cusps_tensor[1, :, :], torch.sub(l_0_tensor, l_1_tensor)), + ), + ), + ) + for _ in range(steps_outer): + dl_tensor = torch.sub(l_1_tensor, l_0_tensor) + dc_tensor = c_1_tensor + k_lms_matrix = _matrix_tensor(tuple(row[1:] for row in _OKLAB_TO_LMS_CUBE_ROOT_MATRIX), units_ab_tensor) + k_lms_tensor = torch.einsum("tc, cwh -> twh", k_lms_matrix, units_ab_tensor) + lms_dt_tensor = torch.add(torch.mul(k_lms_tensor, dc_tensor), dl_tensor) + for _ in range(steps): + l_tensor = torch.add( + torch.mul(l_0_tensor, torch.add(torch.mul(t_tensor, -1.0), 1.0)), torch.mul(t_tensor, l_1_tensor) + ) + c_tensor = torch.mul(t_tensor, c_1_tensor) + root_lms_tensor = torch.add(torch.mul(k_lms_tensor, c_tensor), l_tensor) + lms_tensor = torch.pow(root_lms_tensor, 3.0) + lms_dt_tensor_1 = torch.mul(torch.mul(torch.pow(root_lms_tensor, 2.0), lms_dt_tensor), 3.0) + lms_dt2_tensor = torch.mul(torch.mul(torch.pow(lms_dt_tensor, 2.0), root_lms_tensor), 6.0) + rgb_matrix = _matrix_tensor(_LMS_TO_LINEAR_SRGB_MATRIX, units_ab_tensor) + rgb_tensor = torch.sub(torch.einsum("qt, twh -> qwh", rgb_matrix, lms_tensor), 1.0) + rgb_tensor_1 = torch.einsum("qt, twh -> qwh", rgb_matrix, lms_dt_tensor_1) + rgb_tensor_2 = torch.einsum("qt, twh -> qwh", rgb_matrix, lms_dt2_tensor) + u_rgb_tensor = torch.div( + rgb_tensor_1, + torch.sub(torch.pow(rgb_tensor_1, 2.0), torch.mul(torch.mul(rgb_tensor, rgb_tensor_2), 0.5)), + ) + t_rgb_tensor = torch.mul(torch.mul(rgb_tensor, -1.0), u_rgb_tensor) + max_floats = torch.mul( + MAX_FLOAT, torch.ones(t_rgb_tensor.shape, dtype=t_rgb_tensor.dtype, device=t_rgb_tensor.device) + ) + t_rgb_tensor = torch.where(torch.lt(u_rgb_tensor, 0.0), max_floats, t_rgb_tensor) + t_tensor = torch.where( + torch.gt(cond_tensor, 0.0), torch.add(t_tensor, t_rgb_tensor.min(0).values), t_tensor + ) + return t_tensor + + +def gamut_clip_tensor( + rgb_l_tensor: torch.Tensor, alpha: float = 0.05, steps: int = 1, steps_outer: int = 1 +) -> torch.Tensor: + rgb_l_tensor = _require_color_tensor(rgb_l_tensor) + lab_tensor = oklab_from_linear_srgb(rgb_l_tensor) + epsilon = 0.00001 + chroma_tensor = torch.sqrt(torch.add(torch.pow(lab_tensor[1, :, :], 2.0), torch.pow(lab_tensor[2, :, :], 2.0))) + chroma_tensor = torch.where(torch.lt(chroma_tensor, epsilon), epsilon, chroma_tensor) + units_ab_tensor = torch.div(lab_tensor[1:, :, :], chroma_tensor) + l_d_tensor = torch.sub(lab_tensor[0], 0.5) + e_1_tensor = torch.add(torch.add(torch.abs(l_d_tensor), torch.mul(chroma_tensor, alpha)), 0.5) + l_0_tensor = torch.mul( + torch.add( + torch.mul( + torch.sign(l_d_tensor), + torch.sub( + e_1_tensor, torch.sqrt(torch.sub(torch.pow(e_1_tensor, 2.0), torch.mul(torch.abs(l_d_tensor), 2.0))) + ), + ), + 1.0, + ), + 0.5, + ) + t_tensor = _find_gamut_intersection_tensor( + units_ab_tensor, lab_tensor[0, :, :], chroma_tensor, l_0_tensor, steps=steps, steps_outer=steps_outer + ) + l_clipped_tensor = torch.add( + torch.mul(l_0_tensor, torch.add(torch.mul(t_tensor, -1), 1.0)), torch.mul(t_tensor, lab_tensor[0, :, :]) + ) + c_clipped_tensor = torch.mul(t_tensor, chroma_tensor) + return torch.where( + torch.logical_or(torch.gt(rgb_l_tensor.max(0).values, 1.0), torch.lt(rgb_l_tensor.min(0).values, 0.0)), + linear_srgb_from_oklab( + torch.stack( + [ + l_clipped_tensor, + torch.mul(c_clipped_tensor, units_ab_tensor[0, :, :]), + torch.mul(c_clipped_tensor, units_ab_tensor[1, :, :]), + ] + ) + ), + rgb_l_tensor, + ) + + +def _st_cusps_from_lc(lc_cusps_tensor: torch.Tensor) -> torch.Tensor: + return torch.stack( + [ + torch.div(lc_cusps_tensor[1, :, :], lc_cusps_tensor[0, :, :]), + torch.div(lc_cusps_tensor[1, :, :], torch.add(torch.mul(lc_cusps_tensor[0, :, :], -1.0), 1)), + ] + ) + + +def _ok_l_r_from_l_tensor(x_tensor: torch.Tensor) -> torch.Tensor: + k_1 = 0.206 + k_2 = 0.03 + k_3 = (1.0 + k_1) / (1.0 + k_2) + return torch.mul( + torch.add( + torch.sub(torch.mul(x_tensor, k_3), k_1), + torch.sqrt( + torch.add( + torch.pow(torch.sub(torch.mul(x_tensor, k_3), k_1), 2.0), + torch.mul(torch.mul(torch.mul(x_tensor, k_3), k_2), 4.0), + ) + ), + ), + 0.5, + ) + + +def _ok_l_from_lr_tensor(x_tensor: torch.Tensor) -> torch.Tensor: + k_1 = 0.206 + k_2 = 0.03 + k_3 = (1.0 + k_1) / (1.0 + k_2) + return torch.div( + torch.add(torch.pow(x_tensor, 2.0), torch.mul(x_tensor, k_1)), torch.mul(torch.add(x_tensor, k_2), k_3) + ) + + +def srgb_from_okhsv(okhsv_tensor: torch.Tensor, alpha: float = 0.05, steps: int = 1) -> torch.Tensor: + """Convert a 3xHxW Okhsv tensor, with hue in degrees, to gamma-corrected sRGB.""" + + okhsv_tensor = _require_color_tensor(okhsv_tensor) + okhsv_tensor = okhsv_tensor.clone() + okhsv_tensor[1:, ...] = okhsv_tensor[1:, ...].clamp(0.0, 1.0) + unit_hue_tensor = _unit_hue_from_degrees(okhsv_tensor[0, :, :]) + units_ab_tensor = torch.stack( + [torch.cos(torch.mul(unit_hue_tensor, 2.0 * PI)), torch.sin(torch.mul(unit_hue_tensor, 2.0 * PI))] + ) + lc_cusps_tensor = _find_cusp_tensor(units_ab_tensor, steps=steps) + st_max_tensor = _st_cusps_from_lc(lc_cusps_tensor) + s_0_tensor = _full_like_spatial(st_max_tensor, 0.5) + k_tensor = torch.add(torch.mul(torch.div(s_0_tensor, st_max_tensor[0, :, :]), -1.0), 1) + lc_v_base_tensor = torch.add( + s_0_tensor, + torch.sub( + st_max_tensor[1, :, :], torch.mul(st_max_tensor[1, :, :], torch.mul(k_tensor, okhsv_tensor[1, :, :])) + ), + ) + lc_v_tensor = torch.stack( + [ + torch.add(torch.div(torch.mul(torch.mul(okhsv_tensor[1, :, :], s_0_tensor), -1.0), lc_v_base_tensor), 1.0), + torch.div( + torch.mul(torch.mul(okhsv_tensor[1, :, :], st_max_tensor[1, :, :]), s_0_tensor), lc_v_base_tensor + ), + ] + ) + lc_tensor = torch.mul(okhsv_tensor[2, :, :], lc_v_tensor) + l_vt_tensor = _ok_l_from_lr_tensor(lc_v_tensor[0, :, :]) + c_vt_tensor = torch.mul(lc_v_tensor[1, :, :], torch.div(l_vt_tensor, lc_v_tensor[0, :, :])) + l_new_tensor = _ok_l_from_lr_tensor(lc_tensor[0, :, :]) + lc_tensor[1, :, :] = torch.mul(lc_tensor[1, :, :], torch.div(l_new_tensor, lc_tensor[0, :, :])) + lc_tensor[0, :, :] = l_new_tensor + rgb_scale_tensor = linear_srgb_from_oklab( + torch.stack( + [ + l_vt_tensor, + torch.mul(units_ab_tensor[0, :, :], c_vt_tensor), + torch.mul(units_ab_tensor[1, :, :], c_vt_tensor), + ] + ) + ) + scale_l_tensor = torch.pow( + torch.div( + 1.0, + torch.max( + rgb_scale_tensor.max(0).values, + torch.zeros(rgb_scale_tensor.shape[1:], dtype=rgb_scale_tensor.dtype, device=rgb_scale_tensor.device), + ), + ), + 1.0 / 3.0, + ) + lc_tensor = torch.mul(lc_tensor, scale_l_tensor.expand(lc_tensor.shape)) + rgb_tensor = linear_srgb_from_oklab( + torch.stack( + [ + lc_tensor[0, :, :], + torch.mul(units_ab_tensor[0, :, :], lc_tensor[1, :, :]), + torch.mul(units_ab_tensor[1, :, :], lc_tensor[1, :, :]), + ] + ) + ) + rgb_tensor = srgb_from_linear_srgb(gamut_clip_tensor(rgb_tensor, alpha=alpha, steps=steps)) + return torch.where(torch.isnan(rgb_tensor), 0.0, rgb_tensor).clamp(0.0, 1.0) + + +def okhsv_from_srgb(srgb_tensor: torch.Tensor, steps: int = 1) -> torch.Tensor: + """Convert a 3xHxW gamma-corrected sRGB tensor to Okhsv, with hue in degrees.""" + + srgb_tensor = _require_color_tensor(srgb_tensor) + lab_tensor = oklab_from_linear_srgb(linear_srgb_from_srgb(srgb_tensor)) + c_tensor = torch.sqrt(torch.add(torch.pow(lab_tensor[1, :, :], 2.0), torch.pow(lab_tensor[2, :, :], 2.0))) + units_ab_tensor = torch.div(lab_tensor[1:, :, :], c_tensor) + h_tensor = torch.add( + torch.div( + torch.mul(torch.atan2(torch.mul(lab_tensor[2, :, :], -1.0), torch.mul(lab_tensor[1, :, :], -1.0)), 0.5), PI + ), + 0.5, + ) + lc_cusps_tensor = _find_cusp_tensor(units_ab_tensor, steps=steps) + st_max_tensor = _st_cusps_from_lc(lc_cusps_tensor) + s_0_tensor = _full_like_spatial(st_max_tensor, 0.5) + k_tensor = torch.add(torch.mul(torch.div(s_0_tensor, st_max_tensor[0, :, :]), -1.0), 1) + t_tensor = torch.div( + st_max_tensor[1, :, :], torch.add(c_tensor, torch.mul(lab_tensor[0, :, :], st_max_tensor[1, :, :])) + ) + l_v_tensor = torch.mul(t_tensor, lab_tensor[0, :, :]) + c_v_tensor = torch.mul(t_tensor, c_tensor) + l_vt_tensor = _ok_l_from_lr_tensor(l_v_tensor) + c_vt_tensor = torch.mul(c_v_tensor, torch.div(l_vt_tensor, l_v_tensor)) + rgb_scale_tensor = linear_srgb_from_oklab( + torch.stack( + [ + l_vt_tensor, + torch.mul(units_ab_tensor[0, :, :], c_vt_tensor), + torch.mul(units_ab_tensor[1, :, :], c_vt_tensor), + ] + ) + ) + scale_l_tensor = torch.pow( + torch.div( + 1.0, + torch.max( + rgb_scale_tensor.max(0).values, + torch.zeros(rgb_scale_tensor.shape[1:], dtype=rgb_scale_tensor.dtype, device=rgb_scale_tensor.device), + ), + ), + 1.0 / 3.0, + ) + lab_tensor[0, :, :] = torch.div(lab_tensor[0, :, :], scale_l_tensor) + c_tensor = torch.div(c_tensor, scale_l_tensor) + c_tensor = torch.mul(c_tensor, torch.div(_ok_l_r_from_l_tensor(lab_tensor[0, :, :]), lab_tensor[0, :, :])) + lab_tensor[0, :, :] = _ok_l_r_from_l_tensor(lab_tensor[0, :, :]) + v_tensor = torch.div(lab_tensor[0, :, :], l_v_tensor) + s_tensor = torch.div( + torch.mul(torch.add(s_0_tensor, st_max_tensor[1, :, :]), c_v_tensor), + torch.add( + torch.mul(st_max_tensor[1, :, :], s_0_tensor), + torch.mul(st_max_tensor[1, :, :], torch.mul(k_tensor, c_v_tensor)), + ), + ) + hsv_tensor = torch.stack([_degrees_from_unit_hue(h_tensor), s_tensor, v_tensor]) + hsv_tensor = torch.where(torch.isnan(hsv_tensor), 0.0, hsv_tensor) + hsv_tensor[1:, ...] = hsv_tensor[1:, ...].clamp(0.0, 1.0) + return hsv_tensor + + +def _get_st_mid_tensor(units_ab_tensor: torch.Tensor) -> torch.Tensor: + return torch.stack( + [ + torch.add( + torch.div( + 1.0, + torch.add( + torch.add( + torch.mul(units_ab_tensor[1, :, :], 4.15901240), + torch.mul( + units_ab_tensor[0, :, :], + torch.add( + torch.add( + torch.mul(units_ab_tensor[1, :, :], 1.75198401), + torch.mul( + units_ab_tensor[0, :, :], + torch.add( + torch.add( + torch.mul(units_ab_tensor[1, :, :], -10.02301043), + torch.mul( + units_ab_tensor[0, :, :], + torch.add( + torch.add( + torch.mul(units_ab_tensor[1, :, :], 5.38770819), + torch.mul(units_ab_tensor[0, :, :], 4.69891013), + ), + -4.24894561, + ), + ), + ), + -2.13704948, + ), + ), + ), + -2.19557347, + ), + ), + ), + 7.44778970, + ), + ), + 0.11516993, + ), + torch.add( + torch.div( + 1.0, + torch.add( + torch.add( + torch.mul(units_ab_tensor[1, :, :], -0.68124379), + torch.mul( + units_ab_tensor[0, :, :], + torch.add( + torch.add( + torch.mul(units_ab_tensor[1, :, :], 0.90148123), + torch.mul( + units_ab_tensor[0, :, :], + torch.add( + torch.add( + torch.mul(units_ab_tensor[1, :, :], 0.61223990), + torch.mul( + units_ab_tensor[0, :, :], + torch.add( + torch.add( + torch.mul(units_ab_tensor[1, :, :], -0.45399568), + torch.mul(units_ab_tensor[0, :, :], -0.14661872), + ), + 0.00299215, + ), + ), + ), + -0.27087943, + ), + ), + ), + 0.40370612, + ), + ), + ), + 1.61320320, + ), + ), + 0.11239642, + ), + ] + ) + + +def _get_cs_tensor( + l_tensor: torch.Tensor, units_ab_tensor: torch.Tensor, steps: int = 1, steps_outer: int = 1 +) -> torch.Tensor: + lc_cusps_tensor = _find_cusp_tensor(units_ab_tensor, steps=steps) + c_max_tensor = _find_gamut_intersection_tensor( + units_ab_tensor, + l_tensor, + torch.ones(l_tensor.shape, dtype=l_tensor.dtype, device=l_tensor.device), + l_tensor, + lc_cusps_tensor=lc_cusps_tensor, + steps=steps, + steps_outer=steps_outer, + ) + st_max_tensor = _st_cusps_from_lc(lc_cusps_tensor) + k_tensor = torch.div( + c_max_tensor, + torch.min( + torch.mul(l_tensor, st_max_tensor[0, :, :]), + torch.mul(torch.add(torch.mul(l_tensor, -1.0), 1.0), st_max_tensor[1, :, :]), + ), + ) + st_mid_tensor = _get_st_mid_tensor(units_ab_tensor) + c_a_tensor = torch.mul(l_tensor, st_mid_tensor[0, :, :]) + c_b_tensor = torch.mul(torch.add(torch.mul(l_tensor, -1.0), 1.0), st_mid_tensor[1, :, :]) + c_mid_tensor = torch.mul( + torch.mul( + k_tensor, + torch.sqrt( + torch.sqrt( + torch.div( + 1.0, + torch.add( + torch.div(1.0, torch.pow(c_a_tensor, 4.0)), torch.div(1.0, torch.pow(c_b_tensor, 4.0)) + ), + ) + ) + ), + ), + 0.9, + ) + c_a_tensor = torch.mul(l_tensor, 0.4) + c_b_tensor = torch.mul(torch.add(torch.mul(l_tensor, -1.0), 1.0), 0.8) + c_0_tensor = torch.sqrt( + torch.div( + 1.0, torch.add(torch.div(1.0, torch.pow(c_a_tensor, 2.0)), torch.div(1.0, torch.pow(c_b_tensor, 2.0))) + ) + ) + return torch.stack([c_0_tensor, c_mid_tensor, c_max_tensor]) + + +def srgb_from_okhsl( + hsl_tensor: torch.Tensor, alpha: float = 0.05, steps: int = 1, steps_outer: int = 1 +) -> torch.Tensor: + """Convert a 3xHxW Okhsl tensor, with hue in degrees, to gamma-corrected sRGB.""" + + hsl_tensor = _require_color_tensor(hsl_tensor) + hsl_tensor = hsl_tensor.clone() + hsl_tensor[1:, ...] = hsl_tensor[1:, ...].clamp(0.0, 1.0) + l_ones_mask = torch.eq(hsl_tensor[2, :, :], 1.0) + l_zeros_mask = torch.eq(hsl_tensor[2, :, :], 0.0) + l_ones_mask = l_ones_mask.expand(hsl_tensor.shape) + l_zeros_mask = l_zeros_mask.expand(hsl_tensor.shape) + calc_rgb_mask = torch.logical_not(torch.logical_or(l_ones_mask, l_zeros_mask)) + rgb_tensor = torch.empty_like(hsl_tensor) + rgb_tensor = torch.where(l_ones_mask, 1.0, torch.where(l_zeros_mask, 0.0, rgb_tensor)) + unit_hue_tensor = _unit_hue_from_degrees(hsl_tensor[0, :, :]) + units_ab_tensor = torch.stack( + [torch.cos(torch.mul(unit_hue_tensor, 2.0 * PI)), torch.sin(torch.mul(unit_hue_tensor, 2.0 * PI))] + ) + l_tensor = _ok_l_from_lr_tensor(hsl_tensor[2, :, :]) + cs_tensor = _get_cs_tensor(l_tensor, units_ab_tensor, steps=steps, steps_outer=steps_outer) + mid = 0.8 + mid_inv = 1.25 + s_lt_mid_mask = torch.lt(hsl_tensor[1, :, :], mid) + t_tensor = torch.where( + s_lt_mid_mask, + torch.mul(hsl_tensor[1, :, :], mid_inv), + torch.div(torch.sub(hsl_tensor[1, :, :], mid), 1.0 - mid), + ) + k_1_tensor = torch.where( + s_lt_mid_mask, + torch.mul(cs_tensor[0, :, :], mid), + torch.div( + torch.mul(torch.mul(torch.pow(cs_tensor[1, :, :], 2.0), mid_inv**2.0), 1.0 - mid), cs_tensor[0, :, :] + ), + ) + k_2_tensor = torch.where( + s_lt_mid_mask, + torch.add(torch.mul(torch.div(k_1_tensor, cs_tensor[1, :, :]), -1.0), 1.0), + torch.add(torch.mul(torch.div(k_1_tensor, torch.sub(cs_tensor[2, :, :], cs_tensor[1, :, :])), -1.0), 1.0), + ) + c_tensor = torch.div( + torch.mul(t_tensor, k_1_tensor), torch.add(torch.mul(torch.mul(k_2_tensor, t_tensor), -1.0), 1.0) + ) + c_tensor = torch.where(s_lt_mid_mask, c_tensor, torch.add(cs_tensor[1, :, :], c_tensor)) + rgb_tensor = torch.where( + calc_rgb_mask, + linear_srgb_from_oklab( + torch.stack( + [l_tensor, torch.mul(c_tensor, units_ab_tensor[0, :, :]), torch.mul(c_tensor, units_ab_tensor[1, :, :])] + ) + ), + rgb_tensor, + ) + rgb_tensor = srgb_from_linear_srgb(gamut_clip_tensor(rgb_tensor, alpha=alpha, steps=steps, steps_outer=steps_outer)) + return torch.where(torch.isnan(rgb_tensor), 0.0, rgb_tensor).clamp(0.0, 1.0) + + +def okhsl_from_srgb(rgb_tensor: torch.Tensor, steps: int = 1, steps_outer: int = 1) -> torch.Tensor: + """Convert a 3xHxW gamma-corrected sRGB tensor to Okhsl, with hue in degrees.""" + + rgb_tensor = _require_color_tensor(rgb_tensor) + lab_tensor = oklab_from_linear_srgb(linear_srgb_from_srgb(rgb_tensor)) + c_tensor = torch.sqrt(torch.add(torch.pow(lab_tensor[1, :, :], 2.0), torch.pow(lab_tensor[2, :, :], 2.0))) + units_ab_tensor = torch.stack([torch.div(lab_tensor[1, :, :], c_tensor), torch.div(lab_tensor[2, :, :], c_tensor)]) + h_tensor = torch.add( + torch.div( + torch.mul(torch.atan2(torch.mul(lab_tensor[2, :, :], -1.0), torch.mul(lab_tensor[1, :, :], -1.0)), 0.5), PI + ), + 0.5, + ) + cs_tensor = _get_cs_tensor(lab_tensor[0, :, :], units_ab_tensor, steps=steps, steps_outer=steps_outer) + mid = 0.8 + mid_inv = 1.25 + c_lt_c_mid_mask = torch.lt(c_tensor, cs_tensor[1, :, :]) + k_1_tensor = torch.where( + c_lt_c_mid_mask, + torch.mul(cs_tensor[0, :, :], mid), + torch.div(torch.mul(torch.mul(torch.pow(cs_tensor[1, :, :], 2.0), mid_inv**2), 1.0 - mid), cs_tensor[0, :, :]), + ) + k_2_tensor = torch.where( + c_lt_c_mid_mask, + torch.add(torch.mul(torch.div(k_1_tensor, cs_tensor[1, :, :]), -1.0), 1.0), + torch.add(torch.mul(torch.div(k_1_tensor, torch.sub(cs_tensor[2, :, :], cs_tensor[1, :, :])), -1.0), 1.0), + ) + t_tensor = torch.where( + c_lt_c_mid_mask, + torch.div(c_tensor, torch.add(k_1_tensor, torch.mul(k_2_tensor, c_tensor))), + torch.div( + torch.sub(c_tensor, cs_tensor[1, :, :]), + torch.add(k_1_tensor, torch.mul(k_2_tensor, torch.sub(c_tensor, cs_tensor[1, :, :]))), + ), + ) + s_tensor = torch.where(c_lt_c_mid_mask, torch.mul(t_tensor, mid), torch.add(torch.mul(t_tensor, 1.0 - mid), mid)) + l_tensor = _ok_l_r_from_l_tensor(lab_tensor[0, :, :]) + hsl_tensor = torch.stack([_degrees_from_unit_hue(h_tensor), s_tensor, l_tensor]) + hsl_tensor = torch.where(torch.isnan(hsl_tensor), 0.0, hsl_tensor) + hsl_tensor[1:, ...] = hsl_tensor[1:, ...].clamp(0.0, 1.0) + return hsl_tensor + + +def hsl_from_srgb(rgb_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW gamma-corrected sRGB tensor to HSL, with hue in degrees.""" + + rgb_tensor = _require_color_tensor(rgb_tensor) + c_max_tensor = rgb_tensor.max(0).values + c_min_tensor = rgb_tensor.min(0).values + c_sum_tensor = torch.add(c_max_tensor, c_min_tensor) + c_range_tensor = torch.sub(c_max_tensor, c_min_tensor) + l_tensor = torch.div(c_sum_tensor, 2.0) + s_tensor = torch.where( + torch.eq(c_max_tensor, c_min_tensor), + 0.0, + torch.where( + torch.lt(l_tensor, 0.5), + torch.div(c_range_tensor, c_sum_tensor), + torch.div(c_range_tensor, torch.add(torch.mul(torch.add(c_max_tensor, c_min_tensor), -1.0), 2.0)), + ), + ) + rgb_c_tensor = torch.div( + torch.sub(c_max_tensor.expand(rgb_tensor.shape), rgb_tensor), c_range_tensor.expand(rgb_tensor.shape) + ) + h_tensor = torch.where( + torch.eq(c_max_tensor, c_min_tensor), + 0.0, + torch.where( + torch.eq(rgb_tensor[0, :, :], c_max_tensor), + torch.sub(rgb_c_tensor[2, :, :], rgb_c_tensor[1, :, :]), + torch.where( + torch.eq(rgb_tensor[1, :, :], c_max_tensor), + torch.add(torch.sub(rgb_c_tensor[0, :, :], rgb_c_tensor[2, :, :]), 2.0), + torch.add(torch.sub(rgb_c_tensor[1, :, :], rgb_c_tensor[0, :, :]), 4.0), + ), + ), + ) + h_tensor = _degrees_from_unit_hue(torch.remainder(torch.div(h_tensor, 6.0), 1.0)) + return torch.stack([h_tensor, s_tensor, l_tensor]) + + +def srgb_from_hsl(hsl_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW HSL tensor, with hue in degrees, to gamma-corrected sRGB.""" + + hsl_tensor = _require_color_tensor(hsl_tensor) + hsl_tensor = hsl_tensor.clone() + hsl_tensor[1:, ...] = hsl_tensor[1:, ...].clamp(0.0, 1.0) + rgb_tensor = torch.empty_like(hsl_tensor) + s_0_mask = torch.eq(hsl_tensor[1, :, :], 0.0) + rgb_tensor = torch.where( + s_0_mask.expand(rgb_tensor.shape), hsl_tensor[2, :, :].expand(hsl_tensor.shape), rgb_tensor + ) + m2_tensor = torch.where( + torch.le(hsl_tensor[2, :, :], 0.5), + torch.mul(hsl_tensor[2, :, :], torch.add(hsl_tensor[1, :, :], 1.0)), + torch.sub( + torch.add(hsl_tensor[2, :, :], hsl_tensor[1, :, :]), torch.mul(hsl_tensor[2, :, :], hsl_tensor[1, :, :]) + ), + ) + m1_tensor = torch.sub(torch.mul(hsl_tensor[2, :, :], 2.0), m2_tensor) + unit_hue_tensor = _unit_hue_from_degrees(hsl_tensor[0, :, :]) + + def hsl_values(m1_tensor: torch.Tensor, m2_tensor: torch.Tensor, h_tensor: torch.Tensor) -> torch.Tensor: + h_tensor = torch.remainder(h_tensor, 1.0) + result_tensor = m1_tensor.clone() + return torch.where( + torch.lt(h_tensor, 1.0 / 6.0), + torch.add(m1_tensor, torch.mul(torch.sub(m2_tensor, m1_tensor), torch.mul(h_tensor, 6.0))), + torch.where( + torch.lt(h_tensor, 0.5), + m2_tensor, + torch.where( + torch.lt(h_tensor, 2.0 / 3.0), + torch.add( + m1_tensor, + torch.mul( + torch.sub(m2_tensor, m1_tensor), + torch.mul(torch.add(torch.mul(h_tensor, -1.0), 2.0 / 3.0), 6.0), + ), + ), + result_tensor, + ), + ), + ) + + return torch.stack( + [ + hsl_values(m1_tensor, m2_tensor, torch.add(unit_hue_tensor, 1.0 / 3.0)), + hsl_values(m1_tensor, m2_tensor, unit_hue_tensor), + hsl_values(m1_tensor, m2_tensor, torch.sub(unit_hue_tensor, 1.0 / 3.0)), + ] + ) + + +def hsl_from_linear_srgb(linear_srgb_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW linear-light sRGB tensor directly to HSL, with hue in degrees.""" + + linear_srgb_tensor = _require_color_tensor(linear_srgb_tensor) + return hsl_from_srgb(srgb_from_linear_srgb(linear_srgb_tensor)) + + +def linear_srgb_from_hsl(hsl_tensor: torch.Tensor) -> torch.Tensor: + """Convert a 3xHxW HSL tensor, with hue in degrees, directly to linear-light sRGB.""" + + hsl_tensor = _require_color_tensor(hsl_tensor) + return linear_srgb_from_srgb(srgb_from_hsl(hsl_tensor)) diff --git a/invokeai/backend/image_util/composition.py b/invokeai/backend/image_util/composition.py new file mode 100644 index 00000000000..36911eb3227 --- /dev/null +++ b/invokeai/backend/image_util/composition.py @@ -0,0 +1,122 @@ +# TODO: Improve blend modes +# TODO: Add nodes like Hue Adjust for Saturation/Contrast/etc... ? +# TODO: Continue implementing more blend modes/color spaces(?) +# TODO: Custom ICC profiles with PIL.ImageCms? +# TODO: Blend multiple layers all crammed into a tensor(?) or list + +# Copyright (c) 2023 Darren Ringer +# Parts based on Oklab: Copyright (c) 2021 Bj�rn Ottosson +# HSL code based on CPython: Copyright (c) 2001-2023 Python Software Foundation; All Rights Reserved +from math import pi as PI +from pathlib import Path + +import torch +from PIL import Image + +from invokeai.backend.image_util.color_conversion import ( + gamut_clip_tensor, +) +from invokeai.backend.image_util.color_conversion import ( + srgb_from_linear_srgb as shared_srgb_from_linear_srgb, +) +from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor + +MAX_FLOAT = torch.finfo(torch.tensor(1.0).dtype).max + +# CIE Lab to Uniform Perceptual Lab profile is copyright © 2003 Bruce Justin Lindbloom. All rights reserved. +CIELAB_TO_UPLAB_ICC_PATH = Path(__file__).parent / "assets" / "CIELab_to_UPLab.icc" + + +def equivalent_achromatic_lightness(lch_tensor: torch.Tensor): + """Calculate Equivalent Achromatic Lightness accounting for Helmholtz-Kohlrausch effect""" + # As described by High, Green, and Nussbaum (2023): https://doi.org/10.1002/col.22839 + + k = [0.1644, 0.0603, 0.1307, 0.0060] + + h_minus_90 = torch.sub(lch_tensor[2, :, :], 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(lch_tensor[2, :, :])), k[3]) + + f_r = torch.zeros(lch_tensor[0, :, :].shape) + mask_hi = torch.ge(lch_tensor[2, :, :], -1 * (PI / 2.0)) + mask_lo = torch.le(lch_tensor[2, :, :], PI / 2.0) + mask = torch.logical_and(mask_hi, mask_lo) + f_r[mask] = f_r_0[mask] + + l_max = torch.ones(lch_tensor[0, :, :].shape) + l_min = torch.zeros(lch_tensor[0, :, :].shape) + l_adjustment = torch.tensordot(torch.add(f_by, f_r), lch_tensor[1, :, :], dims=([0, 1], [0, 1])) + l_max = torch.add(l_max, l_adjustment) + l_min = torch.add(l_min, l_adjustment) + l_eal_tensor = torch.add(lch_tensor[0, :, :], l_adjustment) + + l_eal_tensor = torch.add( + lch_tensor[0, :, :], torch.tensordot(torch.add(f_by, f_r), lch_tensor[1, :, :], dims=([0, 1], [0, 1])) + ) + l_eal_tensor = torch.div(torch.sub(l_eal_tensor, l_min.min()), l_max.max() - l_min.min()) + + return l_eal_tensor + + +def srgb_from_linear_srgb(linear_srgb_tensor: torch.Tensor, alpha: float = 0.0, steps: int = 1): + """Get gamma-corrected sRGB from a linear-light sRGB image tensor""" + + if 0.0 < alpha: + linear_srgb_tensor = gamut_clip_tensor(linear_srgb_tensor, alpha=alpha, steps=steps) + return shared_srgb_from_linear_srgb(linear_srgb_tensor) + + +def remove_nans(tensor: torch.Tensor, replace_with: float = MAX_FLOAT): + return torch.where(torch.isnan(tensor), replace_with, tensor) + + +def tensor_from_pil_image(img: Image.Image, normalize: bool = False): + return image_resized_to_grid_as_tensor(img, normalize=normalize, multiple_of=1) + + +# PSF LICENSE AGREEMENT FOR PYTHON 3.11.5 + +# 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and +# the Individual or Organization ("Licensee") accessing and otherwise using Python +# 3.11.5 software in source or binary form and its associated documentation. + +# 2. Subject to the terms and conditions of this License Agreement, PSF hereby +# grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +# analyze, test, perform and/or display publicly, prepare derivative works, +# distribute, and otherwise use Python 3.11.5 alone or in any derivative +# version, provided, however, that PSF's License Agreement and PSF's notice of +# copyright, i.e., "Copyright (c) 2001-2023 Python Software Foundation; All Rights +# Reserved" are retained in Python 3.11.5 alone or in any derivative version +# prepared by Licensee. + +# 3. In the event Licensee prepares a derivative work that is based on or +# incorporates Python 3.11.5 or any part thereof, and wants to make the +# derivative work available to others as provided herein, then Licensee hereby +# agrees to include in any such work a brief summary of the changes made to Python +# 3.11.5. + +# 4. PSF is making Python 3.11.5 available to Licensee on an "AS IS" basis. +# PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF +# EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR +# WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE +# USE OF PYTHON 3.11.5 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 3.11.5 +# FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF +# MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 3.11.5, OR ANY DERIVATIVE +# THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +# 6. This License Agreement will automatically terminate upon a material breach of +# its terms and conditions. + +# 7. Nothing in this License Agreement shall be deemed to create any relationship +# of agency, partnership, or joint venture between PSF and Licensee. This License +# Agreement does not grant permission to use PSF trademarks or trade name in a +# trademark sense to endorse or promote products or services of Licensee, or any +# third party. + +# 8. By copying, installing or otherwise using Python 3.11.5, Licensee agrees +# to be bound by the terms and conditions of this License Agreement. +######################################################################################/ diff --git a/invokeai/backend/image_util/content_shuffle.py b/invokeai/backend/image_util/content_shuffle.py new file mode 100644 index 00000000000..76e3dcf7182 --- /dev/null +++ b/invokeai/backend/image_util/content_shuffle.py @@ -0,0 +1,40 @@ +# Adapted from https://github.com/huggingface/controlnet_aux + +import cv2 +import numpy as np +from PIL import Image + +from invokeai.backend.image_util.util import np_to_pil, pil_to_np + + +def make_noise_disk(H, W, C, F): + noise = np.random.uniform(low=0, high=1, size=((H // F) + 2, (W // F) + 2, C)) + noise = cv2.resize(noise, (W + 2 * F, H + 2 * F), interpolation=cv2.INTER_CUBIC) + noise = noise[F : F + H, F : F + W] + noise -= np.min(noise) + noise /= np.max(noise) + if C == 1: + noise = noise[:, :, None] + return noise + + +def content_shuffle(input_image: Image.Image, scale_factor: int | None = None) -> Image.Image: + """Shuffles the content of an image using a disk noise pattern, similar to a 'liquify' effect.""" + + np_img = pil_to_np(input_image) + + height, width, _channels = np_img.shape + + if scale_factor is None: + scale_factor = 256 + + x = make_noise_disk(height, width, 1, scale_factor) * float(width - 1) + y = make_noise_disk(height, width, 1, scale_factor) * float(height - 1) + + flow = np.concatenate([x, y], axis=2).astype(np.float32) + + shuffled_img = cv2.remap(np_img, flow, None, cv2.INTER_LINEAR) + + output_img = np_to_pil(shuffled_img) + + return output_img diff --git a/invokeai/backend/image_util/controlnet_processor.py b/invokeai/backend/image_util/controlnet_processor.py new file mode 100644 index 00000000000..81eed420977 --- /dev/null +++ b/invokeai/backend/image_util/controlnet_processor.py @@ -0,0 +1,188 @@ +"""Utilities for processing images with ControlNet processors.""" + +from datetime import datetime +from typing import Any, Optional + +from invokeai.app.invocations.fields import ImageField +from invokeai.app.services.invoker import InvocationServices +from invokeai.app.services.session_queue.session_queue_common import SessionQueueItem +from invokeai.app.services.shared.graph import Graph, GraphExecutionState +from invokeai.app.services.shared.invocation_context import InvocationContextData, build_invocation_context + + +def _get_processor_invocation_class(processor_type: str): + """Get the invocation class for a processor type.""" + # Import processor invocation classes on demand + processor_class_map = { + "canny_image_processor": lambda: ( + __import__( + "invokeai.app.invocations.canny", fromlist=["CannyEdgeDetectionInvocation"] + ).CannyEdgeDetectionInvocation + ), + "hed_image_processor": lambda: ( + __import__( + "invokeai.app.invocations.hed", fromlist=["HEDEdgeDetectionInvocation"] + ).HEDEdgeDetectionInvocation + ), + "mlsd_image_processor": lambda: ( + __import__("invokeai.app.invocations.mlsd", fromlist=["MLSDDetectionInvocation"]).MLSDDetectionInvocation + ), + "depth_anything_image_processor": lambda: ( + __import__( + "invokeai.app.invocations.depth_anything", fromlist=["DepthAnythingDepthEstimationInvocation"] + ).DepthAnythingDepthEstimationInvocation + ), + "normalbae_image_processor": lambda: ( + __import__("invokeai.app.invocations.normal_bae", fromlist=["NormalMapInvocation"]).NormalMapInvocation + ), + "pidi_image_processor": lambda: ( + __import__( + "invokeai.app.invocations.pidi", fromlist=["PiDiNetEdgeDetectionInvocation"] + ).PiDiNetEdgeDetectionInvocation + ), + "lineart_image_processor": lambda: ( + __import__( + "invokeai.app.invocations.lineart", fromlist=["LineartEdgeDetectionInvocation"] + ).LineartEdgeDetectionInvocation + ), + "lineart_anime_image_processor": lambda: ( + __import__( + "invokeai.app.invocations.lineart_anime", fromlist=["LineartAnimeEdgeDetectionInvocation"] + ).LineartAnimeEdgeDetectionInvocation + ), + "content_shuffle_image_processor": lambda: ( + __import__( + "invokeai.app.invocations.content_shuffle", fromlist=["ContentShuffleInvocation"] + ).ContentShuffleInvocation + ), + "dw_openpose_image_processor": lambda: ( + __import__( + "invokeai.app.invocations.dw_openpose", fromlist=["DWOpenposeDetectionInvocation"] + ).DWOpenposeDetectionInvocation + ), + "mediapipe_face_processor": lambda: ( + __import__( + "invokeai.app.invocations.mediapipe_face", fromlist=["MediaPipeFaceDetectionInvocation"] + ).MediaPipeFaceDetectionInvocation + ), + # Note: zoe_depth_image_processor doesn't have a processor invocation implementation + "color_map_image_processor": lambda: ( + __import__("invokeai.app.invocations.color_map", fromlist=["ColorMapInvocation"]).ColorMapInvocation + ), + } + + if processor_type in processor_class_map: + return processor_class_map[processor_type]() + return None + + +# Map processor type names to their default parameters +PROCESSOR_DEFAULT_PARAMS = { + "canny_image_processor": {"low_threshold": 100, "high_threshold": 200}, + "hed_image_processor": {"scribble": False}, + "mlsd_image_processor": {"detect_resolution": 512, "thr_v": 0.1, "thr_d": 0.1}, + "depth_anything_image_processor": {"model_size": "small"}, + "normalbae_image_processor": {"detect_resolution": 512}, + "pidi_image_processor": {"detect_resolution": 512, "safe": False}, + "lineart_image_processor": {"detect_resolution": 512, "coarse": False}, + "lineart_anime_image_processor": {"detect_resolution": 512}, + "content_shuffle": {}, + "dw_openpose_image_processor": {"draw_body": True, "draw_face": True, "draw_hands": True}, + "mediapipe_face_processor": {"max_faces": 1, "min_confidence": 0.5}, + "zoe_depth_image_processor": {}, + "color_map_image_processor": {"color_map_tile_size": 64}, +} + + +def process_controlnet_image(image_name: str, model_key: str, services: InvocationServices) -> Optional[dict[str, Any]]: + """ + Process a controlnet image using the appropriate processor based on the model's default settings. + + Args: + image_name: The filename of the image to process + model_key: The model key to look up default processor settings + services: The invocation services providing access to models and images + + Returns: + A dictionary with the processed image data (image_name, width, height) or None if processing fails + """ + logger = services.logger + + try: + # Get model config to find default processor + model_record = services.model_manager.store.get_model(model_key) + if not model_record or not model_record.default_settings: + logger.info(f"No default processor settings found for model {model_key}") + return None + + preprocessor = model_record.default_settings.preprocessor + if not preprocessor: + logger.info(f"No preprocessor configured for model {model_key}") + return None + + # Get the invocation class for this processor + invocation_class = _get_processor_invocation_class(preprocessor) + if not invocation_class: + logger.info(f"No processor mapping found for preprocessor '{preprocessor}'") + return None + + # Get default parameters for this processor + default_params = PROCESSOR_DEFAULT_PARAMS.get(preprocessor, {}) + logger.info(f"Processing image {image_name} with processor {preprocessor}") + + # Create a minimal context to run the invocation + # We need a fake queue item and session for the context + fake_session = GraphExecutionState(graph=Graph()) + now = datetime.now() + + # Create invocation instance first so we have its ID + invocation_params = {"image": ImageField(image_name=image_name), **default_params} + invocation = invocation_class(**invocation_params) + + # Add the invocation ID to the session's prepared_source_mapping + # This is required for the invocation context to emit progress events + fake_session.prepared_source_mapping[invocation.id] = invocation.id + + fake_queue_item = SessionQueueItem( + item_id=0, + session_id=fake_session.id, + queue_id="default", + batch_id="recall_processor", + field_values=None, + session=fake_session, + status="in_progress", + created_at=now, + updated_at=now, + started_at=now, + completed_at=None, + ) + + context_data = InvocationContextData( + invocation=invocation, + source_invocation_id=invocation.id, + queue_item=fake_queue_item, + ) + + context = build_invocation_context( + data=context_data, + services=services, + is_canceled=lambda: False, + ) + + # Invoke the processor + output = invocation.invoke(context) + + # Get the processed image DTO + processed_image_dto = services.images.get_dto(output.image.image_name) + + logger.info(f"Successfully processed image {image_name} -> {processed_image_dto.image_name}") + + return { + "image_name": processed_image_dto.image_name, + "width": processed_image_dto.width, + "height": processed_image_dto.height, + } + + except Exception as e: + logger.error(f"Error processing controlnet image {image_name}: {e}", exc_info=True) + return None diff --git a/invokeai/backend/image_util/depth_anything/depth_anything_pipeline.py b/invokeai/backend/image_util/depth_anything/depth_anything_pipeline.py new file mode 100644 index 00000000000..732aa9ceced --- /dev/null +++ b/invokeai/backend/image_util/depth_anything/depth_anything_pipeline.py @@ -0,0 +1,41 @@ +import pathlib +from typing import Optional + +import torch +from PIL import Image +from transformers import pipeline +from transformers.pipelines import DepthEstimationPipeline + +from invokeai.backend.raw_model import RawModel + + +class DepthAnythingPipeline(RawModel): + """Custom wrapper for the Depth Estimation pipeline from transformers adding compatibility + for Invoke's Model Management System""" + + def __init__(self, pipeline: DepthEstimationPipeline) -> None: + self._pipeline = pipeline + + def generate_depth(self, image: Image.Image) -> Image.Image: + depth_map = self._pipeline(image)["depth"] + assert isinstance(depth_map, Image.Image) + return depth_map + + def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None): + if device is not None and device.type not in {"cpu", "cuda"}: + device = None + self._pipeline.model.to(device=device, dtype=dtype) + self._pipeline.device = self._pipeline.model.device + + def calc_size(self) -> int: + from invokeai.backend.model_manager.load.model_util import calc_module_size + + return calc_module_size(self._pipeline.model) + + @classmethod + def load_model(cls, model_path: pathlib.Path): + """Load the model from the given path and return a DepthAnythingPipeline instance.""" + + depth_anything_pipeline = pipeline(model=str(model_path), task="depth-estimation", local_files_only=True) + assert isinstance(depth_anything_pipeline, DepthEstimationPipeline) + return cls(depth_anything_pipeline) diff --git a/invokeai/backend/image_util/dw_openpose/__init__.py b/invokeai/backend/image_util/dw_openpose/__init__.py new file mode 100644 index 00000000000..dafee63adec --- /dev/null +++ b/invokeai/backend/image_util/dw_openpose/__init__.py @@ -0,0 +1,152 @@ +from pathlib import Path +from typing import Dict + +import huggingface_hub +import numpy as np +import onnxruntime as ort +import torch +from PIL import Image + +from invokeai.backend.image_util.dw_openpose.onnxdet import inference_detector +from invokeai.backend.image_util.dw_openpose.onnxpose import inference_pose +from invokeai.backend.image_util.dw_openpose.utils import NDArrayInt, draw_bodypose, draw_facepose, draw_handpose +from invokeai.backend.image_util.util import np_to_pil +from invokeai.backend.util.devices import TorchDevice + + +class DWOpenposeDetector: + """ + Code from the original implementation of the DW Openpose Detector. + Credits: https://github.com/IDEA-Research/DWPose + """ + + hf_repo_id = "yzd-v/DWPose" + hf_filename_onnx_det = "yolox_l.onnx" + hf_filename_onnx_pose = "dw-ll_ucoco_384.onnx" + + @classmethod + def get_model_url_det(cls) -> str: + """Returns the URL for the detection model.""" + return huggingface_hub.hf_hub_url(cls.hf_repo_id, cls.hf_filename_onnx_det) + + @classmethod + def get_model_url_pose(cls) -> str: + """Returns the URL for the pose model.""" + return huggingface_hub.hf_hub_url(cls.hf_repo_id, cls.hf_filename_onnx_pose) + + @staticmethod + def create_onnx_inference_session(model_path: Path) -> ort.InferenceSession: + """Creates an ONNX Inference Session for the given model path, using the appropriate execution provider based on + the device type.""" + + device = TorchDevice.choose_torch_device() + providers = ["CUDAExecutionProvider"] if device.type == "cuda" else ["CPUExecutionProvider"] + return ort.InferenceSession(path_or_bytes=model_path, providers=providers) + + def __init__(self, session_det: ort.InferenceSession, session_pose: ort.InferenceSession): + self.session_det = session_det + self.session_pose = session_pose + + def pose_estimation(self, np_image: np.ndarray): + """Does the pose estimation on the given image and returns the keypoints and scores.""" + + det_result = inference_detector(self.session_det, np_image) + keypoints, scores = inference_pose(self.session_pose, det_result, np_image) + + keypoints_info = np.concatenate((keypoints, scores[..., None]), axis=-1) + # compute neck joint + neck = np.mean(keypoints_info[:, [5, 6]], axis=1) + # neck score when visualizing pred + neck[:, 2:4] = np.logical_and(keypoints_info[:, 5, 2:4] > 0.3, keypoints_info[:, 6, 2:4] > 0.3).astype(int) + new_keypoints_info = np.insert(keypoints_info, 17, neck, axis=1) + mmpose_idx = [17, 6, 8, 10, 7, 9, 12, 14, 16, 13, 15, 2, 1, 4, 3] + openpose_idx = [1, 2, 3, 4, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17] + new_keypoints_info[:, openpose_idx] = new_keypoints_info[:, mmpose_idx] + keypoints_info = new_keypoints_info + + keypoints, scores = keypoints_info[..., :2], keypoints_info[..., 2] + + return keypoints, scores + + def run( + self, + image: Image.Image, + draw_face: bool = False, + draw_body: bool = True, + draw_hands: bool = False, + ) -> Image.Image: + """Detects the pose in the given image and returns an solid black image with pose drawn on top, suitable for + use with a ControlNet.""" + + np_image = np.array(image) + H, W, C = np_image.shape + + with torch.no_grad(): + candidate, subset = self.pose_estimation(np_image) + nums, keys, locs = candidate.shape + candidate[..., 0] /= float(W) + candidate[..., 1] /= float(H) + body = candidate[:, :18].copy() + body = body.reshape(nums * 18, locs) + score = subset[:, :18] + for i in range(len(score)): + for j in range(len(score[i])): + if score[i][j] > 0.3: + score[i][j] = int(18 * i + j) + else: + score[i][j] = -1 + + un_visible = subset < 0.3 + candidate[un_visible] = -1 + + # foot = candidate[:, 18:24] + + faces = candidate[:, 24:92] + + hands = candidate[:, 92:113] + hands = np.vstack([hands, candidate[:, 113:]]) + + bodies = {"candidate": body, "subset": score} + pose = {"bodies": bodies, "hands": hands, "faces": faces} + + return DWOpenposeDetector.draw_pose( + pose, H, W, draw_face=draw_face, draw_hands=draw_hands, draw_body=draw_body + ) + + @staticmethod + def draw_pose( + pose: Dict[str, NDArrayInt | Dict[str, NDArrayInt]], + H: int, + W: int, + draw_face: bool = True, + draw_body: bool = True, + draw_hands: bool = True, + ) -> Image.Image: + """Draws the pose on a black image and returns it as a PIL Image.""" + + bodies = pose["bodies"] + faces = pose["faces"] + hands = pose["hands"] + + assert isinstance(bodies, dict) + candidate = bodies["candidate"] + + assert isinstance(bodies, dict) + subset = bodies["subset"] + + canvas = np.zeros(shape=(H, W, 3), dtype=np.uint8) + + if draw_body: + canvas = draw_bodypose(canvas, candidate, subset) + + if draw_hands: + assert isinstance(hands, np.ndarray) + canvas = draw_handpose(canvas, hands) + + if draw_face: + assert isinstance(hands, np.ndarray) + canvas = draw_facepose(canvas, faces) # type: ignore + + dwpose_image = np_to_pil(canvas) + + return dwpose_image diff --git a/invokeai/backend/image_util/dw_openpose/onnxdet.py b/invokeai/backend/image_util/dw_openpose/onnxdet.py new file mode 100644 index 00000000000..3706bb8fa3d --- /dev/null +++ b/invokeai/backend/image_util/dw_openpose/onnxdet.py @@ -0,0 +1,128 @@ +# Code from the original DWPose Implementation: https://github.com/IDEA-Research/DWPose + +import cv2 +import numpy as np + + +def nms(boxes, scores, nms_thr): + """Single class NMS implemented in Numpy.""" + x1 = boxes[:, 0] + y1 = boxes[:, 1] + x2 = boxes[:, 2] + y2 = boxes[:, 3] + + areas = (x2 - x1 + 1) * (y2 - y1 + 1) + order = scores.argsort()[::-1] + + keep = [] + while order.size > 0: + i = order[0] + keep.append(i) + xx1 = np.maximum(x1[i], x1[order[1:]]) + yy1 = np.maximum(y1[i], y1[order[1:]]) + xx2 = np.minimum(x2[i], x2[order[1:]]) + yy2 = np.minimum(y2[i], y2[order[1:]]) + + w = np.maximum(0.0, xx2 - xx1 + 1) + h = np.maximum(0.0, yy2 - yy1 + 1) + inter = w * h + ovr = inter / (areas[i] + areas[order[1:]] - inter) + + inds = np.where(ovr <= nms_thr)[0] + order = order[inds + 1] + + return keep + + +def multiclass_nms(boxes, scores, nms_thr, score_thr): + """Multiclass NMS implemented in Numpy. Class-aware version.""" + final_dets = [] + num_classes = scores.shape[1] + for cls_ind in range(num_classes): + cls_scores = scores[:, cls_ind] + valid_score_mask = cls_scores > score_thr + if valid_score_mask.sum() == 0: + continue + else: + valid_scores = cls_scores[valid_score_mask] + valid_boxes = boxes[valid_score_mask] + keep = nms(valid_boxes, valid_scores, nms_thr) + if len(keep) > 0: + cls_inds = np.ones((len(keep), 1)) * cls_ind + dets = np.concatenate([valid_boxes[keep], valid_scores[keep, None], cls_inds], 1) + final_dets.append(dets) + if len(final_dets) == 0: + return None + return np.concatenate(final_dets, 0) + + +def demo_postprocess(outputs, img_size, p6=False): + grids = [] + expanded_strides = [] + strides = [8, 16, 32] if not p6 else [8, 16, 32, 64] + + hsizes = [img_size[0] // stride for stride in strides] + wsizes = [img_size[1] // stride for stride in strides] + + for hsize, wsize, stride in zip(hsizes, wsizes, strides, strict=False): + xv, yv = np.meshgrid(np.arange(wsize), np.arange(hsize)) + grid = np.stack((xv, yv), 2).reshape(1, -1, 2) + grids.append(grid) + shape = grid.shape[:2] + expanded_strides.append(np.full((*shape, 1), stride)) + + grids = np.concatenate(grids, 1) + expanded_strides = np.concatenate(expanded_strides, 1) + outputs[..., :2] = (outputs[..., :2] + grids) * expanded_strides + outputs[..., 2:4] = np.exp(outputs[..., 2:4]) * expanded_strides + + return outputs + + +def preprocess(img, input_size, swap=(2, 0, 1)): + if len(img.shape) == 3: + padded_img = np.ones((input_size[0], input_size[1], 3), dtype=np.uint8) * 114 + else: + padded_img = np.ones(input_size, dtype=np.uint8) * 114 + + r = min(input_size[0] / img.shape[0], input_size[1] / img.shape[1]) + resized_img = cv2.resize( + img, + (int(img.shape[1] * r), int(img.shape[0] * r)), + interpolation=cv2.INTER_LINEAR, + ).astype(np.uint8) + padded_img[: int(img.shape[0] * r), : int(img.shape[1] * r)] = resized_img + + padded_img = padded_img.transpose(swap) + padded_img = np.ascontiguousarray(padded_img, dtype=np.float32) + return padded_img, r + + +def inference_detector(session, oriImg): + input_shape = (640, 640) + img, ratio = preprocess(oriImg, input_shape) + + ort_inputs = {session.get_inputs()[0].name: img[None, :, :, :]} + output = session.run(None, ort_inputs) + predictions = demo_postprocess(output[0], input_shape)[0] + + boxes = predictions[:, :4] + scores = predictions[:, 4:5] * predictions[:, 5:] + + boxes_xyxy = np.ones_like(boxes) + boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2.0 + boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2.0 + boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2.0 + boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2.0 + boxes_xyxy /= ratio + dets = multiclass_nms(boxes_xyxy, scores, nms_thr=0.45, score_thr=0.1) + if dets is not None: + final_boxes, final_scores, final_cls_inds = dets[:, :4], dets[:, 4], dets[:, 5] + isscore = final_scores > 0.3 + iscat = final_cls_inds == 0 + isbbox = [i and j for (i, j) in zip(isscore, iscat, strict=False)] + final_boxes = final_boxes[isbbox] + else: + final_boxes = np.array([]) + + return final_boxes diff --git a/invokeai/backend/image_util/dw_openpose/onnxpose.py b/invokeai/backend/image_util/dw_openpose/onnxpose.py new file mode 100644 index 00000000000..d949f95801b --- /dev/null +++ b/invokeai/backend/image_util/dw_openpose/onnxpose.py @@ -0,0 +1,361 @@ +# Code from the original DWPose Implementation: https://github.com/IDEA-Research/DWPose + +from typing import List, Tuple + +import cv2 +import numpy as np +import onnxruntime as ort + + +def preprocess( + img: np.ndarray, out_bbox, input_size: Tuple[int, int] = (192, 256) +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Do preprocessing for RTMPose model inference. + + Args: + img (np.ndarray): Input image in shape. + input_size (tuple): Input image size in shape (w, h). + + Returns: + tuple: + - resized_img (np.ndarray): Preprocessed image. + - center (np.ndarray): Center of image. + - scale (np.ndarray): Scale of image. + """ + # get shape of image + img_shape = img.shape[:2] + out_img, out_center, out_scale = [], [], [] + if len(out_bbox) == 0: + out_bbox = [[0, 0, img_shape[1], img_shape[0]]] + for i in range(len(out_bbox)): + x0 = out_bbox[i][0] + y0 = out_bbox[i][1] + x1 = out_bbox[i][2] + y1 = out_bbox[i][3] + bbox = np.array([x0, y0, x1, y1]) + + # get center and scale + center, scale = bbox_xyxy2cs(bbox, padding=1.25) + + # do affine transformation + resized_img, scale = top_down_affine(input_size, scale, center, img) + + # normalize image + mean = np.array([123.675, 116.28, 103.53]) + std = np.array([58.395, 57.12, 57.375]) + resized_img = (resized_img - mean) / std + + out_img.append(resized_img) + out_center.append(center) + out_scale.append(scale) + + return out_img, out_center, out_scale + + +def inference(sess: ort.InferenceSession, img: np.ndarray) -> np.ndarray: + """Inference RTMPose model. + + Args: + sess (ort.InferenceSession): ONNXRuntime session. + img (np.ndarray): Input image in shape. + + Returns: + outputs (np.ndarray): Output of RTMPose model. + """ + all_out = [] + # build input + for i in range(len(img)): + input = [img[i].transpose(2, 0, 1)] + + # build output + sess_input = {sess.get_inputs()[0].name: input} + sess_output = [] + for out in sess.get_outputs(): + sess_output.append(out.name) + + # run model + outputs = sess.run(sess_output, sess_input) + all_out.append(outputs) + + return all_out + + +def postprocess( + outputs: List[np.ndarray], + model_input_size: Tuple[int, int], + center: Tuple[int, int], + scale: Tuple[int, int], + simcc_split_ratio: float = 2.0, +) -> Tuple[np.ndarray, np.ndarray]: + """Postprocess for RTMPose model output. + + Args: + outputs (np.ndarray): Output of RTMPose model. + model_input_size (tuple): RTMPose model Input image size. + center (tuple): Center of bbox in shape (x, y). + scale (tuple): Scale of bbox in shape (w, h). + simcc_split_ratio (float): Split ratio of simcc. + + Returns: + tuple: + - keypoints (np.ndarray): Rescaled keypoints. + - scores (np.ndarray): Model predict scores. + """ + all_key = [] + all_score = [] + for i in range(len(outputs)): + # use simcc to decode + simcc_x, simcc_y = outputs[i] + keypoints, scores = decode(simcc_x, simcc_y, simcc_split_ratio) + + # rescale keypoints + keypoints = keypoints / model_input_size * scale[i] + center[i] - scale[i] / 2 + all_key.append(keypoints[0]) + all_score.append(scores[0]) + + return np.array(all_key), np.array(all_score) + + +def bbox_xyxy2cs(bbox: np.ndarray, padding: float = 1.0) -> Tuple[np.ndarray, np.ndarray]: + """Transform the bbox format from (x,y,w,h) into (center, scale) + + Args: + bbox (ndarray): Bounding box(es) in shape (4,) or (n, 4), formatted + as (left, top, right, bottom) + padding (float): BBox padding factor that will be multilied to scale. + Default: 1.0 + + Returns: + tuple: A tuple containing center and scale. + - np.ndarray[float32]: Center (x, y) of the bbox in shape (2,) or + (n, 2) + - np.ndarray[float32]: Scale (w, h) of the bbox in shape (2,) or + (n, 2) + """ + # convert single bbox from (4, ) to (1, 4) + dim = bbox.ndim + if dim == 1: + bbox = bbox[None, :] + + # get bbox center and scale + x1, y1, x2, y2 = np.hsplit(bbox, [1, 2, 3]) + center = np.hstack([x1 + x2, y1 + y2]) * 0.5 + scale = np.hstack([x2 - x1, y2 - y1]) * padding + + if dim == 1: + center = center[0] + scale = scale[0] + + return center, scale + + +def _fix_aspect_ratio(bbox_scale: np.ndarray, aspect_ratio: float) -> np.ndarray: + """Extend the scale to match the given aspect ratio. + + Args: + scale (np.ndarray): The image scale (w, h) in shape (2, ) + aspect_ratio (float): The ratio of ``w/h`` + + Returns: + np.ndarray: The reshaped image scale in (2, ) + """ + w, h = np.hsplit(bbox_scale, [1]) + bbox_scale = np.where(w > h * aspect_ratio, np.hstack([w, w / aspect_ratio]), np.hstack([h * aspect_ratio, h])) + return bbox_scale + + +def _rotate_point(pt: np.ndarray, angle_rad: float) -> np.ndarray: + """Rotate a point by an angle. + + Args: + pt (np.ndarray): 2D point coordinates (x, y) in shape (2, ) + angle_rad (float): rotation angle in radian + + Returns: + np.ndarray: Rotated point in shape (2, ) + """ + sn, cs = np.sin(angle_rad), np.cos(angle_rad) + rot_mat = np.array([[cs, -sn], [sn, cs]]) + return rot_mat @ pt + + +def _get_3rd_point(a: np.ndarray, b: np.ndarray) -> np.ndarray: + """To calculate the affine matrix, three pairs of points are required. This + function is used to get the 3rd point, given 2D points a & b. + + The 3rd point is defined by rotating vector `a - b` by 90 degrees + anticlockwise, using b as the rotation center. + + Args: + a (np.ndarray): The 1st point (x,y) in shape (2, ) + b (np.ndarray): The 2nd point (x,y) in shape (2, ) + + Returns: + np.ndarray: The 3rd point. + """ + direction = a - b + c = b + np.r_[-direction[1], direction[0]] + return c + + +def get_warp_matrix( + center: np.ndarray, + scale: np.ndarray, + rot: float, + output_size: Tuple[int, int], + shift: Tuple[float, float] = (0.0, 0.0), + inv: bool = False, +) -> np.ndarray: + """Calculate the affine transformation matrix that can warp the bbox area + in the input image to the output size. + + Args: + center (np.ndarray[2, ]): Center of the bounding box (x, y). + scale (np.ndarray[2, ]): Scale of the bounding box + wrt [width, height]. + rot (float): Rotation angle (degree). + output_size (np.ndarray[2, ] | list(2,)): Size of the + destination heatmaps. + shift (0-100%): Shift translation ratio wrt the width/height. + Default (0., 0.). + inv (bool): Option to inverse the affine transform direction. + (inv=False: src->dst or inv=True: dst->src) + + Returns: + np.ndarray: A 2x3 transformation matrix + """ + shift = np.array(shift) + src_w = scale[0] + dst_w = output_size[0] + dst_h = output_size[1] + + # compute transformation matrix + rot_rad = np.deg2rad(rot) + src_dir = _rotate_point(np.array([0.0, src_w * -0.5]), rot_rad) + dst_dir = np.array([0.0, dst_w * -0.5]) + + # get four corners of the src rectangle in the original image + src = np.zeros((3, 2), dtype=np.float32) + src[0, :] = center + scale * shift + src[1, :] = center + src_dir + scale * shift + src[2, :] = _get_3rd_point(src[0, :], src[1, :]) + + # get four corners of the dst rectangle in the input image + dst = np.zeros((3, 2), dtype=np.float32) + dst[0, :] = [dst_w * 0.5, dst_h * 0.5] + dst[1, :] = np.array([dst_w * 0.5, dst_h * 0.5]) + dst_dir + dst[2, :] = _get_3rd_point(dst[0, :], dst[1, :]) + + if inv: + warp_mat = cv2.getAffineTransform(np.float32(dst), np.float32(src)) + else: + warp_mat = cv2.getAffineTransform(np.float32(src), np.float32(dst)) + + return warp_mat + + +def top_down_affine( + input_size: dict, bbox_scale: dict, bbox_center: dict, img: np.ndarray +) -> Tuple[np.ndarray, np.ndarray]: + """Get the bbox image as the model input by affine transform. + + Args: + input_size (dict): The input size of the model. + bbox_scale (dict): The bbox scale of the img. + bbox_center (dict): The bbox center of the img. + img (np.ndarray): The original image. + + Returns: + tuple: A tuple containing center and scale. + - np.ndarray[float32]: img after affine transform. + - np.ndarray[float32]: bbox scale after affine transform. + """ + w, h = input_size + warp_size = (int(w), int(h)) + + # reshape bbox to fixed aspect ratio + bbox_scale = _fix_aspect_ratio(bbox_scale, aspect_ratio=w / h) + + # get the affine matrix + center = bbox_center + scale = bbox_scale + rot = 0 + warp_mat = get_warp_matrix(center, scale, rot, output_size=(w, h)) + + # do affine transform + img = cv2.warpAffine(img, warp_mat, warp_size, flags=cv2.INTER_LINEAR) + + return img, bbox_scale + + +def get_simcc_maximum(simcc_x: np.ndarray, simcc_y: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Get maximum response location and value from simcc representations. + + Note: + instance number: N + num_keypoints: K + heatmap height: H + heatmap width: W + + Args: + simcc_x (np.ndarray): x-axis SimCC in shape (K, Wx) or (N, K, Wx) + simcc_y (np.ndarray): y-axis SimCC in shape (K, Wy) or (N, K, Wy) + + Returns: + tuple: + - locs (np.ndarray): locations of maximum heatmap responses in shape + (K, 2) or (N, K, 2) + - vals (np.ndarray): values of maximum heatmap responses in shape + (K,) or (N, K) + """ + N, K, Wx = simcc_x.shape + simcc_x = simcc_x.reshape(N * K, -1) + simcc_y = simcc_y.reshape(N * K, -1) + + # get maximum value locations + x_locs = np.argmax(simcc_x, axis=1) + y_locs = np.argmax(simcc_y, axis=1) + locs = np.stack((x_locs, y_locs), axis=-1).astype(np.float32) + max_val_x = np.amax(simcc_x, axis=1) + max_val_y = np.amax(simcc_y, axis=1) + + # get maximum value across x and y axis + mask = max_val_x > max_val_y + max_val_x[mask] = max_val_y[mask] + vals = max_val_x + locs[vals <= 0.0] = -1 + + # reshape + locs = locs.reshape(N, K, 2) + vals = vals.reshape(N, K) + + return locs, vals + + +def decode(simcc_x: np.ndarray, simcc_y: np.ndarray, simcc_split_ratio) -> Tuple[np.ndarray, np.ndarray]: + """Modulate simcc distribution with Gaussian. + + Args: + simcc_x (np.ndarray[K, Wx]): model predicted simcc in x. + simcc_y (np.ndarray[K, Wy]): model predicted simcc in y. + simcc_split_ratio (int): The split ratio of simcc. + + Returns: + tuple: A tuple containing center and scale. + - np.ndarray[float32]: keypoints in shape (K, 2) or (n, K, 2) + - np.ndarray[float32]: scores in shape (K,) or (n, K) + """ + keypoints, scores = get_simcc_maximum(simcc_x, simcc_y) + keypoints /= simcc_split_ratio + + return keypoints, scores + + +def inference_pose(session, out_bbox, oriImg): + h, w = session.get_inputs()[0].shape[2:] + model_input_size = (w, h) + resized_img, center, scale = preprocess(oriImg, out_bbox, model_input_size) + outputs = inference(session, resized_img) + keypoints, scores = postprocess(outputs, model_input_size, center, scale) + + return keypoints, scores diff --git a/invokeai/backend/image_util/dw_openpose/utils.py b/invokeai/backend/image_util/dw_openpose/utils.py new file mode 100644 index 00000000000..060f6857c73 --- /dev/null +++ b/invokeai/backend/image_util/dw_openpose/utils.py @@ -0,0 +1,158 @@ +# Code from the original DWPose Implementation: https://github.com/IDEA-Research/DWPose + +import math + +import cv2 +import numpy as np +import numpy.typing as npt + +eps = 0.01 +NDArrayInt = npt.NDArray[np.uint8] + + +def draw_bodypose(canvas: NDArrayInt, candidate: NDArrayInt, subset: NDArrayInt) -> NDArrayInt: + H, W, C = canvas.shape + candidate = np.array(candidate) + subset = np.array(subset) + + stickwidth = 4 + + limbSeq = [ + [2, 3], + [2, 6], + [3, 4], + [4, 5], + [6, 7], + [7, 8], + [2, 9], + [9, 10], + [10, 11], + [2, 12], + [12, 13], + [13, 14], + [2, 1], + [1, 15], + [15, 17], + [1, 16], + [16, 18], + [3, 17], + [6, 18], + ] + + colors = [ + [255, 0, 0], + [255, 85, 0], + [255, 170, 0], + [255, 255, 0], + [170, 255, 0], + [85, 255, 0], + [0, 255, 0], + [0, 255, 85], + [0, 255, 170], + [0, 255, 255], + [0, 170, 255], + [0, 85, 255], + [0, 0, 255], + [85, 0, 255], + [170, 0, 255], + [255, 0, 255], + [255, 0, 170], + [255, 0, 85], + ] + + for i in range(17): + for n in range(len(subset)): + index = subset[n][np.array(limbSeq[i]) - 1] + if -1 in index: + continue + Y = candidate[index.astype(int), 0] * float(W) + X = candidate[index.astype(int), 1] * float(H) + mX = np.mean(X) + mY = np.mean(Y) + length = ((X[0] - X[1]) ** 2 + (Y[0] - Y[1]) ** 2) ** 0.5 + angle = math.degrees(math.atan2(X[0] - X[1], Y[0] - Y[1])) + polygon = cv2.ellipse2Poly((int(mY), int(mX)), (int(length / 2), stickwidth), int(angle), 0, 360, 1) + cv2.fillConvexPoly(canvas, polygon, colors[i]) + + canvas = (canvas * 0.6).astype(np.uint8) + + for i in range(18): + for n in range(len(subset)): + index = int(subset[n][i]) + if index == -1: + continue + x, y = candidate[index][0:2] + x = int(x * W) + y = int(y * H) + cv2.circle(canvas, (int(x), int(y)), 4, colors[i], thickness=-1) + + return canvas + + +def draw_handpose(canvas: NDArrayInt, all_hand_peaks: NDArrayInt) -> NDArrayInt: + H, W, C = canvas.shape + + edges = [ + [0, 1], + [1, 2], + [2, 3], + [3, 4], + [0, 5], + [5, 6], + [6, 7], + [7, 8], + [0, 9], + [9, 10], + [10, 11], + [11, 12], + [0, 13], + [13, 14], + [14, 15], + [15, 16], + [0, 17], + [17, 18], + [18, 19], + [19, 20], + ] + + for peaks in all_hand_peaks: + peaks = np.array(peaks) + + for ie, e in enumerate(edges): + x1, y1 = peaks[e[0]] + x2, y2 = peaks[e[1]] + x1 = int(x1 * W) + y1 = int(y1 * H) + x2 = int(x2 * W) + y2 = int(y2 * H) + if x1 > eps and y1 > eps and x2 > eps and y2 > eps: + hsv_color = np.array([[[ie / float(len(edges)) * 180, 255, 255]]], dtype=np.uint8) + rgb_color = cv2.cvtColor(hsv_color, cv2.COLOR_HSV2RGB)[0, 0] + cv2.line( + canvas, + (x1, y1), + (x2, y2), + rgb_color.tolist(), + thickness=2, + ) + + for _, keyponit in enumerate(peaks): + x, y = keyponit + x = int(x * W) + y = int(y * H) + if x > eps and y > eps: + cv2.circle(canvas, (x, y), 4, (0, 0, 255), thickness=-1) + return canvas + + +def draw_facepose(canvas: NDArrayInt, all_lmks: NDArrayInt) -> NDArrayInt: + H, W, C = canvas.shape + for lmks in all_lmks: + lmks = np.array(lmks) + for lmk in lmks: + x, y = lmk + x = int(x * W) + y = int(y * H) + if x > eps and y > eps: + cv2.circle(canvas, (x, y), 3, (255, 255, 255), thickness=-1) + return canvas diff --git a/invokeai/backend/image_util/grounding_dino/__init__.py b/invokeai/backend/image_util/grounding_dino/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/image_util/grounding_dino/detection_result.py b/invokeai/backend/image_util/grounding_dino/detection_result.py new file mode 100644 index 00000000000..2d0c78e6812 --- /dev/null +++ b/invokeai/backend/image_util/grounding_dino/detection_result.py @@ -0,0 +1,22 @@ +from pydantic import BaseModel, ConfigDict + + +class BoundingBox(BaseModel): + """Bounding box helper class.""" + + xmin: int + ymin: int + xmax: int + ymax: int + + +class DetectionResult(BaseModel): + """Detection result from Grounding DINO.""" + + score: float + label: str + box: BoundingBox + model_config = ConfigDict( + # Allow arbitrary types for mask, since it will be a numpy array. + arbitrary_types_allowed=True + ) diff --git a/invokeai/backend/image_util/grounding_dino/grounding_dino_pipeline.py b/invokeai/backend/image_util/grounding_dino/grounding_dino_pipeline.py new file mode 100644 index 00000000000..772e8c0dd85 --- /dev/null +++ b/invokeai/backend/image_util/grounding_dino/grounding_dino_pipeline.py @@ -0,0 +1,37 @@ +from typing import Optional + +import torch +from PIL import Image +from transformers.pipelines import ZeroShotObjectDetectionPipeline + +from invokeai.backend.image_util.grounding_dino.detection_result import DetectionResult +from invokeai.backend.raw_model import RawModel + + +class GroundingDinoPipeline(RawModel): + """A wrapper class for a ZeroShotObjectDetectionPipeline that makes it compatible with the model manager's memory + management system. + """ + + def __init__(self, pipeline: ZeroShotObjectDetectionPipeline): + self._pipeline = pipeline + + def detect(self, image: Image.Image, candidate_labels: list[str], threshold: float = 0.1) -> list[DetectionResult]: + results = self._pipeline(image=image, candidate_labels=candidate_labels, threshold=threshold) + assert results is not None + results = [DetectionResult.model_validate(result) for result in results] + return results + + def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None): + # HACK(ryand): The GroundingDinoPipeline does not work on MPS devices. We only allow it to be moved to CPU or + # CUDA. + if device is not None and device.type not in {"cpu", "cuda"}: + device = None + self._pipeline.model.to(device=device, dtype=dtype) + self._pipeline.device = self._pipeline.model.device + + def calc_size(self) -> int: + # HACK(ryand): Fix the circular import issue. + from invokeai.backend.model_manager.load.model_util import calc_module_size + + return calc_module_size(self._pipeline.model) diff --git a/invokeai/backend/image_util/hed.py b/invokeai/backend/image_util/hed.py new file mode 100644 index 00000000000..a2d3449f650 --- /dev/null +++ b/invokeai/backend/image_util/hed.py @@ -0,0 +1,217 @@ +# Adapted from https://github.com/huggingface/controlnet_aux + +import pathlib + +import cv2 +import huggingface_hub +import numpy as np +import torch +from einops import rearrange +from huggingface_hub import hf_hub_download +from PIL import Image + +from invokeai.backend.image_util.util import ( + nms, + normalize_image_channel_count, + np_to_pil, + pil_to_np, + resize_image_to_resolution, + safe_step, +) +from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device + + +class DoubleConvBlock(torch.nn.Module): + def __init__(self, input_channel, output_channel, layer_number): + super().__init__() + self.convs = torch.nn.Sequential() + self.convs.append( + torch.nn.Conv2d( + in_channels=input_channel, out_channels=output_channel, kernel_size=(3, 3), stride=(1, 1), padding=1 + ) + ) + for _i in range(1, layer_number): + self.convs.append( + torch.nn.Conv2d( + in_channels=output_channel, + out_channels=output_channel, + kernel_size=(3, 3), + stride=(1, 1), + padding=1, + ) + ) + self.projection = torch.nn.Conv2d( + in_channels=output_channel, out_channels=1, kernel_size=(1, 1), stride=(1, 1), padding=0 + ) + + def __call__(self, x, down_sampling=False): + h = x + if down_sampling: + h = torch.nn.functional.max_pool2d(h, kernel_size=(2, 2), stride=(2, 2)) + for conv in self.convs: + h = conv(h) + h = torch.nn.functional.relu(h) + return h, self.projection(h) + + +class ControlNetHED_Apache2(torch.nn.Module): + def __init__(self): + super().__init__() + self.norm = torch.nn.Parameter(torch.zeros(size=(1, 3, 1, 1))) + self.block1 = DoubleConvBlock(input_channel=3, output_channel=64, layer_number=2) + self.block2 = DoubleConvBlock(input_channel=64, output_channel=128, layer_number=2) + self.block3 = DoubleConvBlock(input_channel=128, output_channel=256, layer_number=3) + self.block4 = DoubleConvBlock(input_channel=256, output_channel=512, layer_number=3) + self.block5 = DoubleConvBlock(input_channel=512, output_channel=512, layer_number=3) + + def __call__(self, x): + h = x - self.norm + h, projection1 = self.block1(h) + h, projection2 = self.block2(h, down_sampling=True) + h, projection3 = self.block3(h, down_sampling=True) + h, projection4 = self.block4(h, down_sampling=True) + h, projection5 = self.block5(h, down_sampling=True) + return projection1, projection2, projection3, projection4, projection5 + + +class HEDProcessor: + """Holistically-Nested Edge Detection. + + On instantiation, loads the HED model from the HuggingFace Hub. + """ + + def __init__(self): + model_path = hf_hub_download("lllyasviel/Annotators", "ControlNetHED.pth") + self.network = ControlNetHED_Apache2() + self.network.load_state_dict(torch.load(model_path, map_location="cpu")) + self.network.float().eval() + + def to(self, device: torch.device): + self.network.to(device) + return self + + def run( + self, + input_image: Image.Image, + detect_resolution: int = 512, + image_resolution: int = 512, + safe: bool = False, + scribble: bool = False, + ) -> Image.Image: + """Processes an image and returns the detected edges. + + Args: + input_image: The input image. + detect_resolution: The resolution to fit the image to before edge detection. + image_resolution: The resolution to fit the edges to before returning. + safe: Whether to apply safe step to the detected edges. + scribble: Whether to apply non-maximum suppression and Gaussian blur to the detected edges. + + Returns: + The detected edges. + """ + device = get_effective_device(self.network) + np_image = pil_to_np(input_image) + np_image = normalize_image_channel_count(np_image) + np_image = resize_image_to_resolution(np_image, detect_resolution) + + assert np_image.ndim == 3 + height, width, _channels = np_image.shape + with torch.no_grad(): + image_hed = torch.from_numpy(np_image.copy()).float().to(device) + image_hed = rearrange(image_hed, "h w c -> 1 c h w") + edges = self.network(image_hed) + edges = [e.detach().cpu().numpy().astype(np.float32)[0, 0] for e in edges] + edges = [cv2.resize(e, (width, height), interpolation=cv2.INTER_LINEAR) for e in edges] + edges = np.stack(edges, axis=2) + edge = 1 / (1 + np.exp(-np.mean(edges, axis=2).astype(np.float64))) + if safe: + edge = safe_step(edge) + edge = (edge * 255.0).clip(0, 255).astype(np.uint8) + + detected_map = edge + detected_map = normalize_image_channel_count(detected_map) + + img = resize_image_to_resolution(np_image, image_resolution) + height, width, _channels = img.shape + + detected_map = cv2.resize(detected_map, (width, height), interpolation=cv2.INTER_LINEAR) + + if scribble: + detected_map = nms(detected_map, 127, 3.0) + detected_map = cv2.GaussianBlur(detected_map, (0, 0), 3.0) + detected_map[detected_map > 4] = 255 + detected_map[detected_map < 255] = 0 + + return np_to_pil(detected_map) + + +class HEDEdgeDetector: + """Simple wrapper around the HED model for detecting edges in an image.""" + + hf_repo_id = "lllyasviel/Annotators" + hf_filename = "ControlNetHED.pth" + + def __init__(self, model: ControlNetHED_Apache2): + self.model = model + + @classmethod + def get_model_url(cls) -> str: + """Get the URL to download the model from the Hugging Face Hub.""" + return huggingface_hub.hf_hub_url(cls.hf_repo_id, cls.hf_filename) + + @classmethod + def load_model(cls, model_path: pathlib.Path) -> ControlNetHED_Apache2: + """Load the model from a file.""" + model = ControlNetHED_Apache2() + model.load_state_dict(torch.load(model_path, map_location="cpu")) + model.float().eval() + return model + + def to(self, device: torch.device): + self.model.to(device) + return self + + def run(self, image: Image.Image, safe: bool = False, scribble: bool = False) -> Image.Image: + """Processes an image and returns the detected edges. + + Args: + image: The input image. + safe: Whether to apply safe step to the detected edges. + scribble: Whether to apply non-maximum suppression and Gaussian blur to the detected edges. + + Returns: + The detected edges. + """ + + device = get_effective_device(self.model) + + np_image = pil_to_np(image) + + height, width, _channels = np_image.shape + + with torch.no_grad(): + image_hed = torch.from_numpy(np_image.copy()).float().to(device) + image_hed = rearrange(image_hed, "h w c -> 1 c h w") + edges = self.model(image_hed) + edges = [e.detach().cpu().numpy().astype(np.float32)[0, 0] for e in edges] + edges = [cv2.resize(e, (width, height), interpolation=cv2.INTER_LINEAR) for e in edges] + edges = np.stack(edges, axis=2) + edge = 1 / (1 + np.exp(-np.mean(edges, axis=2).astype(np.float64))) + if safe: + edge = safe_step(edge) + edge = (edge * 255.0).clip(0, 255).astype(np.uint8) + + detected_map = edge + + detected_map = cv2.resize(detected_map, (width, height), interpolation=cv2.INTER_LINEAR) + + if scribble: + detected_map = nms(detected_map, 127, 3.0) + detected_map = cv2.GaussianBlur(detected_map, (0, 0), 3.0) + detected_map[detected_map > 4] = 255 + detected_map[detected_map < 255] = 0 + + output = np_to_pil(detected_map) + + return output diff --git a/invokeai/backend/image_util/imwatermark/vendor.py b/invokeai/backend/image_util/imwatermark/vendor.py new file mode 100644 index 00000000000..bb072307d64 --- /dev/null +++ b/invokeai/backend/image_util/imwatermark/vendor.py @@ -0,0 +1,310 @@ +# This file is vendored from https://github.com/ShieldMnt/invisible-watermark +# +# `invisible-watermark` is MIT licensed as of August 23, 2025, when the code was copied into this repo. +# +# Why we vendored it in: +# `invisible-watermark` has a dependency on `opencv-python`, which conflicts with Invoke's dependency on +# `opencv-contrib-python`. It's easier to copy the code over than complicate the installation process by +# requiring an extra post-install step of removing `opencv-python` and installing `opencv-contrib-python`. + +import struct +import uuid +import base64 +import cv2 +import numpy as np +import pywt + + +class WatermarkEncoder(object): + def __init__(self, content=b""): + seq = np.array([n for n in content], dtype=np.uint8) + self._watermarks = list(np.unpackbits(seq)) + self._wmLen = len(self._watermarks) + self._wmType = "bytes" + + def set_by_ipv4(self, addr): + bits = [] + ips = addr.split(".") + for ip in ips: + bits += list(np.unpackbits(np.array([ip % 255], dtype=np.uint8))) + self._watermarks = bits + self._wmLen = len(self._watermarks) + self._wmType = "ipv4" + assert self._wmLen == 32 + + def set_by_uuid(self, uid): + u = uuid.UUID(uid) + self._wmType = "uuid" + seq = np.array([n for n in u.bytes], dtype=np.uint8) + self._watermarks = list(np.unpackbits(seq)) + self._wmLen = len(self._watermarks) + + def set_by_bytes(self, content): + self._wmType = "bytes" + seq = np.array([n for n in content], dtype=np.uint8) + self._watermarks = list(np.unpackbits(seq)) + self._wmLen = len(self._watermarks) + + def set_by_b16(self, b16): + content = base64.b16decode(b16) + self.set_by_bytes(content) + self._wmType = "b16" + + def set_by_bits(self, bits=None): + if bits is None: + bits = [] + self._watermarks = [int(bit) % 2 for bit in bits] + self._wmLen = len(self._watermarks) + self._wmType = "bits" + + def set_watermark(self, wmType="bytes", content=""): + if wmType == "ipv4": + self.set_by_ipv4(content) + elif wmType == "uuid": + self.set_by_uuid(content) + elif wmType == "bits": + self.set_by_bits(content) + elif wmType == "bytes": + self.set_by_bytes(content) + elif wmType == "b16": + self.set_by_b16(content) + else: + raise NameError("%s is not supported" % wmType) + + def get_length(self): + return self._wmLen + + # @classmethod + # def loadModel(cls): + # RivaWatermark.loadModel() + + def encode(self, cv2Image, method="dwtDct", **configs): + (r, c, channels) = cv2Image.shape + if r * c < 256 * 256: + raise RuntimeError("image too small, should be larger than 256x256") + + if method == "dwtDct": + embed = EmbedMaxDct(self._watermarks, wmLen=self._wmLen, **configs) + return embed.encode(cv2Image) + # elif method == 'dwtDctSvd': + # embed = EmbedDwtDctSvd(self._watermarks, wmLen=self._wmLen, **configs) + # return embed.encode(cv2Image) + # elif method == 'rivaGan': + # embed = RivaWatermark(self._watermarks, self._wmLen) + # return embed.encode(cv2Image) + else: + raise NameError("%s is not supported" % method) + + +class WatermarkDecoder(object): + def __init__(self, wm_type="bytes", length=0): + self._wmType = wm_type + if wm_type == "ipv4": + self._wmLen = 32 + elif wm_type == "uuid": + self._wmLen = 128 + elif wm_type == "bytes": + self._wmLen = length + elif wm_type == "bits": + self._wmLen = length + elif wm_type == "b16": + self._wmLen = length + else: + raise NameError("%s is unsupported" % wm_type) + + def reconstruct_ipv4(self, bits): + ips = [str(ip) for ip in list(np.packbits(bits))] + return ".".join(ips) + + def reconstruct_uuid(self, bits): + nums = np.packbits(bits) + bstr = b"" + for i in range(16): + bstr += struct.pack(">B", nums[i]) + + return str(uuid.UUID(bytes=bstr)) + + def reconstruct_bits(self, bits): + # return ''.join([str(b) for b in bits]) + return bits + + def reconstruct_b16(self, bits): + bstr = self.reconstruct_bytes(bits) + return base64.b16encode(bstr) + + def reconstruct_bytes(self, bits): + nums = np.packbits(bits) + bstr = b"" + for i in range(self._wmLen // 8): + bstr += struct.pack(">B", nums[i]) + return bstr + + def reconstruct(self, bits): + if len(bits) != self._wmLen: + raise RuntimeError("bits are not matched with watermark length") + + if self._wmType == "ipv4": + return self.reconstruct_ipv4(bits) + elif self._wmType == "uuid": + return self.reconstruct_uuid(bits) + elif self._wmType == "bits": + return self.reconstruct_bits(bits) + elif self._wmType == "b16": + return self.reconstruct_b16(bits) + else: + return self.reconstruct_bytes(bits) + + def decode(self, cv2Image, method="dwtDct", **configs): + (r, c, channels) = cv2Image.shape + if r * c < 256 * 256: + raise RuntimeError("image too small, should be larger than 256x256") + + bits = [] + if method == "dwtDct": + embed = EmbedMaxDct(watermarks=[], wmLen=self._wmLen, **configs) + bits = embed.decode(cv2Image) + # elif method == 'dwtDctSvd': + # embed = EmbedDwtDctSvd(watermarks=[], wmLen=self._wmLen, **configs) + # bits = embed.decode(cv2Image) + # elif method == 'rivaGan': + # embed = RivaWatermark(watermarks=[], wmLen=self._wmLen, **configs) + # bits = embed.decode(cv2Image) + else: + raise NameError("%s is not supported" % method) + return self.reconstruct(bits) + + # @classmethod + # def loadModel(cls): + # RivaWatermark.loadModel() + + +class EmbedMaxDct(object): + def __init__(self, watermarks=None, wmLen=8, scales=None, block=4): + if watermarks is None: + watermarks = [] + if scales is None: + scales = [0, 36, 36] + self._watermarks = watermarks + self._wmLen = wmLen + self._scales = scales + self._block = block + + def encode(self, bgr): + (row, col, channels) = bgr.shape + + yuv = cv2.cvtColor(bgr, cv2.COLOR_BGR2YUV) + + for channel in range(2): + if self._scales[channel] <= 0: + continue + + ca1, (h1, v1, d1) = pywt.dwt2(yuv[: row // 4 * 4, : col // 4 * 4, channel], "haar") + self.encode_frame(ca1, self._scales[channel]) + + yuv[: row // 4 * 4, : col // 4 * 4, channel] = pywt.idwt2((ca1, (v1, h1, d1)), "haar") + + bgr_encoded = cv2.cvtColor(yuv, cv2.COLOR_YUV2BGR) + return bgr_encoded + + def decode(self, bgr): + (row, col, channels) = bgr.shape + + yuv = cv2.cvtColor(bgr, cv2.COLOR_BGR2YUV) + + scores = [[] for i in range(self._wmLen)] + for channel in range(2): + if self._scales[channel] <= 0: + continue + + ca1, (h1, v1, d1) = pywt.dwt2(yuv[: row // 4 * 4, : col // 4 * 4, channel], "haar") + + scores = self.decode_frame(ca1, self._scales[channel], scores) + + avgScores = list(map(lambda l: np.array(l).mean(), scores)) + + bits = np.array(avgScores) * 255 > 127 + return bits + + def decode_frame(self, frame, scale, scores): + (row, col) = frame.shape + num = 0 + + for i in range(row // self._block): + for j in range(col // self._block): + block = frame[ + i * self._block : i * self._block + self._block, j * self._block : j * self._block + self._block + ] + + score = self.infer_dct_matrix(block, scale) + # score = self.infer_dct_svd(block, scale) + wmBit = num % self._wmLen + scores[wmBit].append(score) + num = num + 1 + + return scores + + def diffuse_dct_svd(self, block, wmBit, scale): + u, s, v = np.linalg.svd(cv2.dct(block)) + + s[0] = (s[0] // scale + 0.25 + 0.5 * wmBit) * scale + return cv2.idct(np.dot(u, np.dot(np.diag(s), v))) + + def infer_dct_svd(self, block, scale): + u, s, v = np.linalg.svd(cv2.dct(block)) + + score = 0 + score = int((s[0] % scale) > scale * 0.5) + return score + if score >= 0.5: + return 1.0 + else: + return 0.0 + + def diffuse_dct_matrix(self, block, wmBit, scale): + pos = np.argmax(abs(block.flatten()[1:])) + 1 + i, j = pos // self._block, pos % self._block + val = block[i][j] + if val >= 0.0: + block[i][j] = (val // scale + 0.25 + 0.5 * wmBit) * scale + else: + val = abs(val) + block[i][j] = -1.0 * (val // scale + 0.25 + 0.5 * wmBit) * scale + return block + + def infer_dct_matrix(self, block, scale): + pos = np.argmax(abs(block.flatten()[1:])) + 1 + i, j = pos // self._block, pos % self._block + + val = block[i][j] + if val < 0: + val = abs(val) + + if (val % scale) > 0.5 * scale: + return 1 + else: + return 0 + + def encode_frame(self, frame, scale): + """ + frame is a matrix (M, N) + + we get K (watermark bits size) blocks (self._block x self._block) + + For i-th block, we encode watermark[i] bit into it + """ + (row, col) = frame.shape + num = 0 + for i in range(row // self._block): + for j in range(col // self._block): + block = frame[ + i * self._block : i * self._block + self._block, j * self._block : j * self._block + self._block + ] + wmBit = self._watermarks[(num % self._wmLen)] + + diffusedBlock = self.diffuse_dct_matrix(block, wmBit, scale) + # diffusedBlock = self.diffuse_dct_svd(block, wmBit, scale) + frame[ + i * self._block : i * self._block + self._block, j * self._block : j * self._block + self._block + ] = diffusedBlock + + num = num + 1 diff --git a/invokeai/backend/image_util/infill_methods/cv2_inpaint.py b/invokeai/backend/image_util/infill_methods/cv2_inpaint.py new file mode 100644 index 00000000000..edc16e3bfbb --- /dev/null +++ b/invokeai/backend/image_util/infill_methods/cv2_inpaint.py @@ -0,0 +1,20 @@ +import cv2 +import numpy as np +from PIL import Image + + +def cv2_inpaint(image: Image.Image) -> Image.Image: + # Prepare Image + image_array = np.array(image.convert("RGB")) + image_cv = cv2.cvtColor(image_array, cv2.COLOR_RGB2BGR) + + # Prepare Mask From Alpha Channel + mask = image.split()[3].convert("RGB") + mask_array = np.array(mask) + mask_cv = cv2.cvtColor(mask_array, cv2.COLOR_BGR2GRAY) + mask_inv = cv2.bitwise_not(mask_cv) + + # Inpaint Image + inpainted_result = cv2.inpaint(image_cv, mask_inv, 3, cv2.INPAINT_TELEA) + inpainted_image = Image.fromarray(cv2.cvtColor(inpainted_result, cv2.COLOR_BGR2RGB)) + return inpainted_image diff --git a/invokeai/backend/image_util/infill_methods/lama.py b/invokeai/backend/image_util/infill_methods/lama.py new file mode 100644 index 00000000000..5b3b6857099 --- /dev/null +++ b/invokeai/backend/image_util/infill_methods/lama.py @@ -0,0 +1,54 @@ +from pathlib import Path +from typing import Any + +import numpy as np +import torch +from PIL import Image + +import invokeai.backend.util.logging as logger +from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device +from invokeai.backend.model_manager.taxonomy import AnyModel + + +def norm_img(np_img): + if len(np_img.shape) == 2: + np_img = np_img[:, :, np.newaxis] + np_img = np.transpose(np_img, (2, 0, 1)) + np_img = np_img.astype("float32") / 255 + return np_img + + +class LaMA: + def __init__(self, model: AnyModel): + self._model = model + + def __call__(self, input_image: Image.Image, *args: Any, **kwds: Any) -> Any: + image = np.asarray(input_image.convert("RGB")) + image = norm_img(image) + + mask = input_image.split()[-1] + mask = np.asarray(mask) + mask = np.invert(mask) + mask = norm_img(mask) + mask = (mask > 0) * 1 + + device = get_effective_device(self._model) + image = torch.from_numpy(image).unsqueeze(0).to(device) + mask = torch.from_numpy(mask).unsqueeze(0).to(device) + + with torch.inference_mode(): + infilled_image = self._model(image, mask) + + infilled_image = infilled_image[0].permute(1, 2, 0).detach().cpu().numpy() + infilled_image = np.clip(infilled_image * 255, 0, 255).astype("uint8") + infilled_image = Image.fromarray(infilled_image) + + return infilled_image + + @staticmethod + def load_jit_model(url_or_path: str | Path, device: torch.device | str = "cpu") -> torch.nn.Module: + model_path = url_or_path + logger.info(f"Loading model from: {model_path}") + model: torch.nn.Module = torch.jit.load(model_path, map_location="cpu").to(device) # type: ignore + model.eval() + return model diff --git a/invokeai/backend/image_util/infill_methods/mosaic.py b/invokeai/backend/image_util/infill_methods/mosaic.py new file mode 100644 index 00000000000..2715a100d28 --- /dev/null +++ b/invokeai/backend/image_util/infill_methods/mosaic.py @@ -0,0 +1,60 @@ +from typing import Tuple + +import numpy as np +from PIL import Image + + +def infill_mosaic( + image: Image.Image, + tile_shape: Tuple[int, int] = (64, 64), + min_color: Tuple[int, int, int, int] = (0, 0, 0, 0), + max_color: Tuple[int, int, int, int] = (255, 255, 255, 0), +) -> Image.Image: + """ + image:PIL - A PIL Image + tile_shape: Tuple[int,int] - Tile width & Tile Height + min_color: Tuple[int,int,int] - RGB values for the lowest color to clip to (0-255) + max_color: Tuple[int,int,int] - RGB values for the highest color to clip to (0-255) + """ + + np_image = np.array(image) # Convert image to np array + alpha = np_image[:, :, 3] # Get the mask from the alpha channel of the image + non_transparent_pixels = np_image[alpha != 0, :3] # List of non-transparent pixels + + # Create color tiles to paste in the empty areas of the image + tile_width, tile_height = tile_shape + + # Clip the range of colors in the image to a particular spectrum only + r_min, g_min, b_min, _ = min_color + r_max, g_max, b_max, _ = max_color + non_transparent_pixels[:, 0] = np.clip(non_transparent_pixels[:, 0], r_min, r_max) + non_transparent_pixels[:, 1] = np.clip(non_transparent_pixels[:, 1], g_min, g_max) + non_transparent_pixels[:, 2] = np.clip(non_transparent_pixels[:, 2], b_min, b_max) + + tiles = [] + for _ in range(256): + color = non_transparent_pixels[np.random.randint(len(non_transparent_pixels))] + tile = np.zeros((tile_height, tile_width, 3), dtype=np.uint8) + tile[:, :] = color + tiles.append(tile) + + # Fill the transparent area with tiles + filled_image = np.zeros((image.height, image.width, 3), dtype=np.uint8) + + for x in range(image.width): + for y in range(image.height): + tile = tiles[np.random.randint(len(tiles))] + try: + filled_image[ + y - (y % tile_height) : y - (y % tile_height) + tile_height, + x - (x % tile_width) : x - (x % tile_width) + tile_width, + ] = tile + except ValueError: + # Need to handle edge cases - literally + pass + + filled_image = Image.fromarray(filled_image) # Convert the filled tiles image to PIL + image = Image.composite( + image, filled_image, image.split()[-1] + ) # Composite the original image on top of the filled tiles + return image diff --git a/invokeai/backend/image_util/infill_methods/patchmatch.py b/invokeai/backend/image_util/infill_methods/patchmatch.py new file mode 100644 index 00000000000..7e9cdf8fa41 --- /dev/null +++ b/invokeai/backend/image_util/infill_methods/patchmatch.py @@ -0,0 +1,67 @@ +""" +This module defines a singleton object, "patchmatch" that +wraps the actual patchmatch object. It respects the global +"try_patchmatch" attribute, so that patchmatch loading can +be suppressed or deferred +""" + +import numpy as np +from PIL import Image + +import invokeai.backend.util.logging as logger +from invokeai.app.services.config.config_default import get_config + + +class PatchMatch: + """ + Thin class wrapper around the patchmatch function. + """ + + patch_match = None + tried_load: bool = False + + def __init__(self): + super().__init__() + + @classmethod + def _load_patch_match(cls): + if cls.tried_load: + return + if get_config().patchmatch: + from patchmatch import patch_match as pm + + if pm.patchmatch_available: + logger.info("Patchmatch initialized") + cls.patch_match = pm + else: + logger.info("Patchmatch not loaded (nonfatal)") + else: + logger.info("Patchmatch loading disabled") + cls.tried_load = True + + @classmethod + def patchmatch_available(cls) -> bool: + cls._load_patch_match() + if not cls.patch_match: + return False + return cls.patch_match.patchmatch_available + + @classmethod + def inpaint(cls, image: Image.Image) -> Image.Image: + if cls.patch_match is None or not cls.patchmatch_available(): + return image + + np_image = np.array(image) + mask = 255 - np_image[:, :, 3] + infilled = cls.patch_match.inpaint(np_image[:, :, :3], mask, patch_size=3) + return Image.fromarray(infilled, mode="RGB") + + +def infill_patchmatch(image: Image.Image) -> Image.Image: + IS_PATCHMATCH_AVAILABLE = PatchMatch.patchmatch_available() + + if not IS_PATCHMATCH_AVAILABLE: + logger.warning("PatchMatch is not available on this system") + return image + + return PatchMatch.inpaint(image) diff --git a/invokeai/backend/image_util/infill_methods/test_images/source1.webp b/invokeai/backend/image_util/infill_methods/test_images/source1.webp new file mode 100644 index 00000000000..7057eefa85f Binary files /dev/null and b/invokeai/backend/image_util/infill_methods/test_images/source1.webp differ diff --git a/invokeai/backend/image_util/infill_methods/test_images/source10.webp b/invokeai/backend/image_util/infill_methods/test_images/source10.webp new file mode 100644 index 00000000000..f185d52a573 Binary files /dev/null and b/invokeai/backend/image_util/infill_methods/test_images/source10.webp differ diff --git a/invokeai/backend/image_util/infill_methods/test_images/source2.webp b/invokeai/backend/image_util/infill_methods/test_images/source2.webp new file mode 100644 index 00000000000..b25060024a7 Binary files /dev/null and b/invokeai/backend/image_util/infill_methods/test_images/source2.webp differ diff --git a/invokeai/backend/image_util/infill_methods/test_images/source3.webp b/invokeai/backend/image_util/infill_methods/test_images/source3.webp new file mode 100644 index 00000000000..64227084c74 Binary files /dev/null and b/invokeai/backend/image_util/infill_methods/test_images/source3.webp differ diff --git a/invokeai/backend/image_util/infill_methods/test_images/source4.webp b/invokeai/backend/image_util/infill_methods/test_images/source4.webp new file mode 100644 index 00000000000..66a4260063a Binary files /dev/null and b/invokeai/backend/image_util/infill_methods/test_images/source4.webp differ diff --git a/invokeai/backend/image_util/infill_methods/test_images/source5.webp b/invokeai/backend/image_util/infill_methods/test_images/source5.webp new file mode 100644 index 00000000000..49b87b268f1 Binary files /dev/null and b/invokeai/backend/image_util/infill_methods/test_images/source5.webp differ diff --git a/invokeai/backend/image_util/infill_methods/test_images/source6.webp b/invokeai/backend/image_util/infill_methods/test_images/source6.webp new file mode 100644 index 00000000000..e16e1320049 Binary files /dev/null and b/invokeai/backend/image_util/infill_methods/test_images/source6.webp differ diff --git a/invokeai/backend/image_util/infill_methods/test_images/source7.webp b/invokeai/backend/image_util/infill_methods/test_images/source7.webp new file mode 100644 index 00000000000..723a5fddbd7 Binary files /dev/null and b/invokeai/backend/image_util/infill_methods/test_images/source7.webp differ diff --git a/invokeai/backend/image_util/infill_methods/test_images/source8.webp b/invokeai/backend/image_util/infill_methods/test_images/source8.webp new file mode 100644 index 00000000000..32a0fea1097 Binary files /dev/null and b/invokeai/backend/image_util/infill_methods/test_images/source8.webp differ diff --git a/invokeai/backend/image_util/infill_methods/test_images/source9.webp b/invokeai/backend/image_util/infill_methods/test_images/source9.webp new file mode 100644 index 00000000000..062f25ed8d5 Binary files /dev/null and b/invokeai/backend/image_util/infill_methods/test_images/source9.webp differ diff --git a/invokeai/backend/image_util/infill_methods/tile.ipynb b/invokeai/backend/image_util/infill_methods/tile.ipynb new file mode 100644 index 00000000000..eac7a436577 --- /dev/null +++ b/invokeai/backend/image_util/infill_methods/tile.ipynb @@ -0,0 +1,95 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"Smoke test for the tile infill\"\"\"\n", + "\n", + "from pathlib import Path\n", + "from typing import Optional\n", + "from PIL import Image\n", + "from invokeai.backend.image_util.infill_methods.tile import infill_tile\n", + "\n", + "images: list[tuple[str, Image.Image]] = []\n", + "\n", + "for i in sorted(Path(\"./test_images/\").glob(\"*.webp\")):\n", + " images.append((i.name, Image.open(i)))\n", + " images.append((i.name, Image.open(i).transpose(Image.FLIP_LEFT_RIGHT)))\n", + " images.append((i.name, Image.open(i).transpose(Image.FLIP_TOP_BOTTOM)))\n", + " images.append((i.name, Image.open(i).resize((512, 512))))\n", + " images.append((i.name, Image.open(i).resize((1234, 461))))\n", + "\n", + "outputs: list[tuple[str, Image.Image, Image.Image, Optional[Image.Image]]] = []\n", + "\n", + "for name, image in images:\n", + " try:\n", + " output = infill_tile(image, seed=0, tile_size=32)\n", + " outputs.append((name, image, output.infilled, output.tile_image))\n", + " except ValueError as e:\n", + " print(f\"Skipping image {name}: {e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Display the images in jupyter notebook\n", + "import matplotlib.pyplot as plt\n", + "from PIL import ImageOps\n", + "\n", + "fig, axes = plt.subplots(len(outputs), 3, figsize=(10, 3 * len(outputs)))\n", + "plt.subplots_adjust(hspace=0)\n", + "\n", + "for i, (name, original, infilled, tile_image) in enumerate(outputs):\n", + " # Add a border to each image, helps to see the edges\n", + " size = original.size\n", + " original = ImageOps.expand(original, border=5, fill=\"red\")\n", + " filled = ImageOps.expand(infilled, border=5, fill=\"red\")\n", + " if tile_image:\n", + " tile_image = ImageOps.expand(tile_image, border=5, fill=\"red\")\n", + "\n", + " axes[i, 0].imshow(original)\n", + " axes[i, 0].axis(\"off\")\n", + " axes[i, 0].set_title(f\"Original ({name} - {size})\")\n", + "\n", + " if tile_image:\n", + " axes[i, 1].imshow(tile_image)\n", + " axes[i, 1].axis(\"off\")\n", + " axes[i, 1].set_title(\"Tile Image\")\n", + " else:\n", + " axes[i, 1].axis(\"off\")\n", + " axes[i, 1].set_title(\"NO TILES GENERATED (NO TRANSPARENCY)\")\n", + "\n", + " axes[i, 2].imshow(filled)\n", + " axes[i, 2].axis(\"off\")\n", + " axes[i, 2].set_title(\"Filled\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".invokeai", + "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.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/invokeai/backend/image_util/infill_methods/tile.py b/invokeai/backend/image_util/infill_methods/tile.py new file mode 100644 index 00000000000..03cb6c1a8c6 --- /dev/null +++ b/invokeai/backend/image_util/infill_methods/tile.py @@ -0,0 +1,122 @@ +from dataclasses import dataclass +from typing import Optional + +import numpy as np +from PIL import Image + + +def create_tile_pool(img_array: np.ndarray, tile_size: tuple[int, int]) -> list[np.ndarray]: + """ + Create a pool of tiles from non-transparent areas of the image by systematically walking through the image. + + Args: + img_array: numpy array of the image. + tile_size: tuple (tile_width, tile_height) specifying the size of each tile. + + Returns: + A list of numpy arrays, each representing a tile. + """ + tiles: list[np.ndarray] = [] + rows, cols = img_array.shape[:2] + tile_width, tile_height = tile_size + + for y in range(0, rows - tile_height + 1, tile_height): + for x in range(0, cols - tile_width + 1, tile_width): + tile = img_array[y : y + tile_height, x : x + tile_width] + # Check if the image has an alpha channel and the tile is completely opaque + if img_array.shape[2] == 4 and np.all(tile[:, :, 3] == 255): + tiles.append(tile) + elif img_array.shape[2] == 3: # If no alpha channel, append the tile + tiles.append(tile) + + if not tiles: + raise ValueError( + "Not enough opaque pixels to generate any tiles. Use a smaller tile size or a different image." + ) + + return tiles + + +def create_filled_image( + img_array: np.ndarray, tile_pool: list[np.ndarray], tile_size: tuple[int, int], seed: int +) -> np.ndarray: + """ + Create an image of the same dimensions as the original, filled entirely with tiles from the pool. + + Args: + img_array: numpy array of the original image. + tile_pool: A list of numpy arrays, each representing a tile. + tile_size: tuple (tile_width, tile_height) specifying the size of each tile. + + Returns: + A numpy array representing the filled image. + """ + + rows, cols, _ = img_array.shape + tile_width, tile_height = tile_size + + # Prep an empty RGB image + filled_img_array = np.zeros((rows, cols, 3), dtype=img_array.dtype) + + # Make the random tile selection reproducible + rng = np.random.default_rng(seed) + + for y in range(0, rows, tile_height): + for x in range(0, cols, tile_width): + # Pick a random tile from the pool + tile = tile_pool[rng.integers(len(tile_pool))] + + # Calculate the space available (may be less than tile size near the edges) + space_y = min(tile_height, rows - y) + space_x = min(tile_width, cols - x) + + # Crop the tile if necessary to fit into the available space + cropped_tile = tile[:space_y, :space_x, :3] + + # Fill the available space with the (possibly cropped) tile + filled_img_array[y : y + space_y, x : x + space_x, :3] = cropped_tile + + return filled_img_array + + +@dataclass +class InfillTileOutput: + infilled: Image.Image + tile_image: Optional[Image.Image] = None + + +def infill_tile(image_to_infill: Image.Image, seed: int, tile_size: int) -> InfillTileOutput: + """Infills an image with random tiles from the image itself. + + If the image is not an RGBA image, it is returned untouched. + + Args: + image: The image to infill. + tile_size: The size of the tiles to use for infilling. + + Raises: + ValueError: If there are not enough opaque pixels to generate any tiles. + """ + + if image_to_infill.mode != "RGBA": + return InfillTileOutput(infilled=image_to_infill) + + # Internally, we want a tuple of (tile_width, tile_height). In the future, the tile size can be any rectangle. + _tile_size = (tile_size, tile_size) + np_image = np.array(image_to_infill, dtype=np.uint8) + + # Create the pool of tiles that we will use to infill + tile_pool = create_tile_pool(np_image, _tile_size) + + # Create an image from the tiles, same size as the original + tile_np_image = create_filled_image(np_image, tile_pool, _tile_size, seed) + + # Paste the OG image over the tile image, effectively infilling the area + tile_image = Image.fromarray(tile_np_image, "RGB") + infilled = tile_image.copy() + infilled.paste(image_to_infill, (0, 0), image_to_infill.split()[-1]) + + # I think we want this to be "RGBA"? + infilled.convert("RGBA") + + return InfillTileOutput(infilled=infilled, tile_image=tile_image) diff --git a/invokeai/backend/image_util/invisible_watermark.py b/invokeai/backend/image_util/invisible_watermark.py new file mode 100644 index 00000000000..95c483848cc --- /dev/null +++ b/invokeai/backend/image_util/invisible_watermark.py @@ -0,0 +1,49 @@ +""" +This module defines a singleton object, "invisible_watermark" that +wraps the invisible watermark model. It respects the global "invisible_watermark" +configuration variable, that allows the watermarking to be supressed. +""" + +import cv2 +import numpy as np +from PIL import Image + +import invokeai.backend.util.logging as logger +from invokeai.backend.image_util.imwatermark.vendor import WatermarkDecoder, WatermarkEncoder + + +class InvisibleWatermark: + """ + Wrapper around InvisibleWatermark module. + """ + + @classmethod + def add_watermark(cls, image: Image.Image, watermark_text: str) -> Image.Image: + logger.debug(f'Applying invisible watermark "{watermark_text}"') + bgr = cv2.cvtColor(np.array(image.convert("RGB")), cv2.COLOR_RGB2BGR) + encoder = WatermarkEncoder() + encoder.set_watermark("bytes", watermark_text.encode("utf-8")) + bgr_encoded = encoder.encode(bgr, "dwtDct") + return Image.fromarray(cv2.cvtColor(bgr_encoded, cv2.COLOR_BGR2RGB)).convert("RGBA") + + @classmethod + def decode_watermark(cls, image: Image.Image, length: int = 8) -> str: + """Attempt to decode an invisible watermark from an image. + + Args: + image: The PIL Image to decode the watermark from. + length: The expected watermark length in bytes. Must match the length used when encoding. + The WatermarkDecoder requires the length in bits; this value is multiplied by 8 internally. + + Returns: + The decoded watermark text, or an empty string if no watermark is detected or decoding fails. + """ + logger.debug("Attempting to decode invisible watermark") + try: + bgr = cv2.cvtColor(np.array(image.convert("RGB")), cv2.COLOR_RGB2BGR) + decoder = WatermarkDecoder("bytes", length * 8) + watermark_bytes = decoder.decode(bgr, "dwtDct") + return watermark_bytes.decode("utf-8", errors="ignore").rstrip("\x00") + except Exception: + logger.debug("Failed to decode invisible watermark") + return "" diff --git a/invokeai/backend/image_util/lineart.py b/invokeai/backend/image_util/lineart.py new file mode 100644 index 00000000000..bfef6f6da08 --- /dev/null +++ b/invokeai/backend/image_util/lineart.py @@ -0,0 +1,228 @@ +"""Adapted from https://github.com/huggingface/controlnet_aux (Apache-2.0 license).""" + +import pathlib + +import cv2 +import huggingface_hub +import numpy as np +import torch +import torch.nn as nn +from einops import rearrange +from huggingface_hub import hf_hub_download +from PIL import Image + +from invokeai.backend.image_util.util import ( + normalize_image_channel_count, + np_to_pil, + pil_to_np, + resize_image_to_resolution, +) +from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device + + +class ResidualBlock(nn.Module): + def __init__(self, in_features): + super(ResidualBlock, self).__init__() + + conv_block = [ + nn.ReflectionPad2d(1), + nn.Conv2d(in_features, in_features, 3), + nn.InstanceNorm2d(in_features), + nn.ReLU(inplace=True), + nn.ReflectionPad2d(1), + nn.Conv2d(in_features, in_features, 3), + nn.InstanceNorm2d(in_features), + ] + + self.conv_block = nn.Sequential(*conv_block) + + def forward(self, x): + return x + self.conv_block(x) + + +class Generator(nn.Module): + def __init__(self, input_nc, output_nc, n_residual_blocks=9, sigmoid=True): + super(Generator, self).__init__() + + # Initial convolution block + model0 = [nn.ReflectionPad2d(3), nn.Conv2d(input_nc, 64, 7), nn.InstanceNorm2d(64), nn.ReLU(inplace=True)] + self.model0 = nn.Sequential(*model0) + + # Downsampling + model1 = [] + in_features = 64 + out_features = in_features * 2 + for _ in range(2): + model1 += [ + nn.Conv2d(in_features, out_features, 3, stride=2, padding=1), + nn.InstanceNorm2d(out_features), + nn.ReLU(inplace=True), + ] + in_features = out_features + out_features = in_features * 2 + self.model1 = nn.Sequential(*model1) + + model2 = [] + # Residual blocks + for _ in range(n_residual_blocks): + model2 += [ResidualBlock(in_features)] + self.model2 = nn.Sequential(*model2) + + # Upsampling + model3 = [] + out_features = in_features // 2 + for _ in range(2): + model3 += [ + nn.ConvTranspose2d(in_features, out_features, 3, stride=2, padding=1, output_padding=1), + nn.InstanceNorm2d(out_features), + nn.ReLU(inplace=True), + ] + in_features = out_features + out_features = in_features // 2 + self.model3 = nn.Sequential(*model3) + + # Output layer + model4 = [nn.ReflectionPad2d(3), nn.Conv2d(64, output_nc, 7)] + if sigmoid: + model4 += [nn.Sigmoid()] + + self.model4 = nn.Sequential(*model4) + + def forward(self, x, cond=None): + out = self.model0(x) + out = self.model1(out) + out = self.model2(out) + out = self.model3(out) + out = self.model4(out) + + return out + + +class LineartProcessor: + """Processor for lineart detection.""" + + def __init__(self): + model_path = hf_hub_download("lllyasviel/Annotators", "sk_model.pth") + self.model = Generator(3, 1, 3) + self.model.load_state_dict(torch.load(model_path, map_location=torch.device("cpu"))) + self.model.eval() + + coarse_model_path = hf_hub_download("lllyasviel/Annotators", "sk_model2.pth") + self.model_coarse = Generator(3, 1, 3) + self.model_coarse.load_state_dict(torch.load(coarse_model_path, map_location=torch.device("cpu"))) + self.model_coarse.eval() + + def to(self, device: torch.device): + self.model.to(device) + self.model_coarse.to(device) + return self + + def run( + self, input_image: Image.Image, coarse: bool = False, detect_resolution: int = 512, image_resolution: int = 512 + ) -> Image.Image: + """Processes an image to detect lineart. + + Args: + input_image: The input image. + coarse: Whether to use the coarse model. + detect_resolution: The resolution to fit the image to before edge detection. + image_resolution: The resolution of the output image. + + Returns: + The detected lineart. + """ + device = get_effective_device(self.model) + + np_image = pil_to_np(input_image) + np_image = normalize_image_channel_count(np_image) + np_image = resize_image_to_resolution(np_image, detect_resolution) + + model = self.model_coarse if coarse else self.model + assert np_image.ndim == 3 + image = np_image + with torch.no_grad(): + image = torch.from_numpy(image).float().to(device) + image = image / 255.0 + image = rearrange(image, "h w c -> 1 c h w") + line = model(image)[0][0] + + line = line.cpu().numpy() + line = (line * 255.0).clip(0, 255).astype(np.uint8) + + detected_map = line + + detected_map = normalize_image_channel_count(detected_map) + + img = resize_image_to_resolution(np_image, image_resolution) + H, W, C = img.shape + + detected_map = cv2.resize(detected_map, (W, H), interpolation=cv2.INTER_LINEAR) + detected_map = 255 - detected_map + + return np_to_pil(detected_map) + + +class LineartEdgeDetector: + """Simple wrapper around the fine and coarse lineart models for detecting edges in an image.""" + + hf_repo_id = "lllyasviel/Annotators" + hf_filename_fine = "sk_model.pth" + hf_filename_coarse = "sk_model2.pth" + + @classmethod + def get_model_url(cls, coarse: bool = False) -> str: + """Get the URL to download the model from the Hugging Face Hub.""" + if coarse: + return huggingface_hub.hf_hub_url(cls.hf_repo_id, cls.hf_filename_coarse) + else: + return huggingface_hub.hf_hub_url(cls.hf_repo_id, cls.hf_filename_fine) + + @classmethod + def load_model(cls, model_path: pathlib.Path) -> Generator: + """Load the model from a file.""" + model = Generator(3, 1, 3) + model.load_state_dict(torch.load(model_path, map_location="cpu")) + model.float().eval() + return model + + def __init__(self, model: Generator) -> None: + self.model = model + + def to(self, device: torch.device): + self.model.to(device) + return self + + def run(self, image: Image.Image) -> Image.Image: + """Detects edges in the input image with the selected lineart model. + + Args: + input: The input image. + coarse: Whether to use the coarse model. + + Returns: + The detected edges. + """ + device = get_effective_device(self.model) + + np_image = pil_to_np(image) + + with torch.no_grad(): + np_image = torch.from_numpy(np_image).float().to(device) + np_image = np_image / 255.0 + np_image = rearrange(np_image, "h w c -> 1 c h w") + line = self.model(np_image)[0][0] + + line = line.cpu().numpy() + line = (line * 255.0).clip(0, 255).astype(np.uint8) + + detected_map = 255 - line + + # The lineart model often outputs a lot of almost-black noise. SD1.5 ControlNets seem to be OK with this, but + # SDXL ControlNets are not - they need a cleaner map. 12 was experimentally determined to be a good threshold, + # eliminating all the noise while keeping the actual edges. Other approaches to thresholding may be better, + # for example stretching the contrast or removing noise. + detected_map[detected_map < 12] = 0 + + output = np_to_pil(detected_map) + + return output diff --git a/invokeai/backend/image_util/lineart_anime.py b/invokeai/backend/image_util/lineart_anime.py new file mode 100644 index 00000000000..fa406cf1d4b --- /dev/null +++ b/invokeai/backend/image_util/lineart_anime.py @@ -0,0 +1,274 @@ +"""Adapted from https://github.com/huggingface/controlnet_aux (Apache-2.0 license).""" + +import functools +import pathlib +from typing import Optional + +import cv2 +import huggingface_hub +import numpy as np +import torch +import torch.nn as nn +from einops import rearrange +from huggingface_hub import hf_hub_download +from PIL import Image + +from invokeai.backend.image_util.util import ( + normalize_image_channel_count, + np_to_pil, + pil_to_np, + resize_image_to_resolution, +) +from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device + + +class UnetGenerator(nn.Module): + """Create a Unet-based generator""" + + def __init__( + self, + input_nc: int, + output_nc: int, + num_downs: int, + ngf: int = 64, + norm_layer=nn.BatchNorm2d, + use_dropout: bool = False, + ): + """Construct a Unet generator + Parameters: + input_nc (int) -- the number of channels in input images + output_nc (int) -- the number of channels in output images + num_downs (int) -- the number of downsamplings in UNet. For example, # if |num_downs| == 7, + image of size 128x128 will become of size 1x1 # at the bottleneck + ngf (int) -- the number of filters in the last conv layer + norm_layer -- normalization layer + We construct the U-Net from the innermost layer to the outermost layer. + It is a recursive process. + """ + super(UnetGenerator, self).__init__() + # construct unet structure + unet_block = UnetSkipConnectionBlock( + ngf * 8, ngf * 8, input_nc=None, submodule=None, norm_layer=norm_layer, innermost=True + ) # add the innermost layer + for _ in range(num_downs - 5): # add intermediate layers with ngf * 8 filters + unet_block = UnetSkipConnectionBlock( + ngf * 8, ngf * 8, input_nc=None, submodule=unet_block, norm_layer=norm_layer, use_dropout=use_dropout + ) + # gradually reduce the number of filters from ngf * 8 to ngf + unet_block = UnetSkipConnectionBlock( + ngf * 4, ngf * 8, input_nc=None, submodule=unet_block, norm_layer=norm_layer + ) + unet_block = UnetSkipConnectionBlock( + ngf * 2, ngf * 4, input_nc=None, submodule=unet_block, norm_layer=norm_layer + ) + unet_block = UnetSkipConnectionBlock(ngf, ngf * 2, input_nc=None, submodule=unet_block, norm_layer=norm_layer) + self.model = UnetSkipConnectionBlock( + output_nc, ngf, input_nc=input_nc, submodule=unet_block, outermost=True, norm_layer=norm_layer + ) # add the outermost layer + + def forward(self, input): + """Standard forward""" + return self.model(input) + + +class UnetSkipConnectionBlock(nn.Module): + """Defines the Unet submodule with skip connection. + X -------------------identity---------------------- + |-- downsampling -- |submodule| -- upsampling --| + """ + + def __init__( + self, + outer_nc: int, + inner_nc: int, + input_nc: Optional[int] = None, + submodule=None, + outermost: bool = False, + innermost: bool = False, + norm_layer=nn.BatchNorm2d, + use_dropout: bool = False, + ): + """Construct a Unet submodule with skip connections. + Parameters: + outer_nc (int) -- the number of filters in the outer conv layer + inner_nc (int) -- the number of filters in the inner conv layer + input_nc (int) -- the number of channels in input images/features + submodule (UnetSkipConnectionBlock) -- previously defined submodules + outermost (bool) -- if this module is the outermost module + innermost (bool) -- if this module is the innermost module + norm_layer -- normalization layer + use_dropout (bool) -- if use dropout layers. + """ + super(UnetSkipConnectionBlock, self).__init__() + self.outermost = outermost + if isinstance(norm_layer, functools.partial): + use_bias = norm_layer.func == nn.InstanceNorm2d + else: + use_bias = norm_layer == nn.InstanceNorm2d + if input_nc is None: + input_nc = outer_nc + downconv = nn.Conv2d(input_nc, inner_nc, kernel_size=4, stride=2, padding=1, bias=use_bias) + downrelu = nn.LeakyReLU(0.2, True) + downnorm = norm_layer(inner_nc) + uprelu = nn.ReLU(True) + upnorm = norm_layer(outer_nc) + + if outermost: + upconv = nn.ConvTranspose2d(inner_nc * 2, outer_nc, kernel_size=4, stride=2, padding=1) + down = [downconv] + up = [uprelu, upconv, nn.Tanh()] + model = down + [submodule] + up + elif innermost: + upconv = nn.ConvTranspose2d(inner_nc, outer_nc, kernel_size=4, stride=2, padding=1, bias=use_bias) + down = [downrelu, downconv] + up = [uprelu, upconv, upnorm] + model = down + up + else: + upconv = nn.ConvTranspose2d(inner_nc * 2, outer_nc, kernel_size=4, stride=2, padding=1, bias=use_bias) + down = [downrelu, downconv, downnorm] + up = [uprelu, upconv, upnorm] + + if use_dropout: + model = down + [submodule] + up + [nn.Dropout(0.5)] + else: + model = down + [submodule] + up + + self.model = nn.Sequential(*model) + + def forward(self, x): + if self.outermost: + return self.model(x) + else: # add skip connections + return torch.cat([x, self.model(x)], 1) + + +class LineartAnimeProcessor: + """Processes an image to detect lineart.""" + + def __init__(self): + model_path = hf_hub_download("lllyasviel/Annotators", "netG.pth") + norm_layer = functools.partial(nn.InstanceNorm2d, affine=False, track_running_stats=False) + self.model = UnetGenerator(3, 1, 8, 64, norm_layer=norm_layer, use_dropout=False) + ckpt = torch.load(model_path) + for key in list(ckpt.keys()): + if "module." in key: + ckpt[key.replace("module.", "")] = ckpt[key] + del ckpt[key] + self.model.load_state_dict(ckpt) + self.model.eval() + + def to(self, device: torch.device): + self.model.to(device) + return self + + def run(self, input_image: Image.Image, detect_resolution: int = 512, image_resolution: int = 512) -> Image.Image: + """Processes an image to detect lineart. + + Args: + input_image: The input image. + detect_resolution: The resolution to use for detection. + image_resolution: The resolution to use for the output image. + + Returns: + The detected lineart. + """ + device = get_effective_device(self.model) + np_image = pil_to_np(input_image) + + np_image = normalize_image_channel_count(np_image) + np_image = resize_image_to_resolution(np_image, detect_resolution) + + H, W, C = np_image.shape + Hn = 256 * int(np.ceil(float(H) / 256.0)) + Wn = 256 * int(np.ceil(float(W) / 256.0)) + img = cv2.resize(np_image, (Wn, Hn), interpolation=cv2.INTER_CUBIC) + with torch.no_grad(): + image_feed = torch.from_numpy(img).float().to(device) + image_feed = image_feed / 127.5 - 1.0 + image_feed = rearrange(image_feed, "h w c -> 1 c h w") + + line = self.model(image_feed)[0, 0] * 127.5 + 127.5 + line = line.cpu().numpy() + + line = cv2.resize(line, (W, H), interpolation=cv2.INTER_CUBIC) + line = line.clip(0, 255).astype(np.uint8) + + detected_map = line + + detected_map = normalize_image_channel_count(detected_map) + + img = resize_image_to_resolution(np_image, image_resolution) + H, W, C = img.shape + + detected_map = cv2.resize(detected_map, (W, H), interpolation=cv2.INTER_LINEAR) + detected_map = 255 - detected_map + + return np_to_pil(detected_map) + + +class LineartAnimeEdgeDetector: + """Simple wrapper around the Lineart Anime model for detecting edges in an image.""" + + hf_repo_id = "lllyasviel/Annotators" + hf_filename = "netG.pth" + + @classmethod + def get_model_url(cls) -> str: + """Get the URL to download the model from the Hugging Face Hub.""" + return huggingface_hub.hf_hub_url(cls.hf_repo_id, cls.hf_filename) + + @classmethod + def load_model(cls, model_path: pathlib.Path) -> UnetGenerator: + """Load the model from a file.""" + norm_layer = functools.partial(nn.InstanceNorm2d, affine=False, track_running_stats=False) + model = UnetGenerator(3, 1, 8, 64, norm_layer=norm_layer, use_dropout=False) + ckpt = torch.load(model_path) + for key in list(ckpt.keys()): + if "module." in key: + ckpt[key.replace("module.", "")] = ckpt[key] + del ckpt[key] + model.load_state_dict(ckpt) + model.eval() + return model + + def __init__(self, model: UnetGenerator) -> None: + self.model = model + + def to(self, device: torch.device): + self.model.to(device) + return self + + def run(self, image: Image.Image) -> Image.Image: + """Processes an image and returns the detected edges.""" + device = get_effective_device(self.model) + + np_image = pil_to_np(image) + + height, width, _channels = np_image.shape + new_height = 256 * int(np.ceil(float(height) / 256.0)) + new_width = 256 * int(np.ceil(float(width) / 256.0)) + + resized_img = cv2.resize(np_image, (new_width, new_height), interpolation=cv2.INTER_CUBIC) + + with torch.no_grad(): + image_feed = torch.from_numpy(resized_img).float().to(device) + image_feed = image_feed / 127.5 - 1.0 + image_feed = rearrange(image_feed, "h w c -> 1 c h w") + + line = self.model(image_feed)[0, 0] * 127.5 + 127.5 + line = line.cpu().numpy() + + line = cv2.resize(line, (width, height), interpolation=cv2.INTER_CUBIC) + line = line.clip(0, 255).astype(np.uint8) + + detected_map = 255 - line + + # The lineart model often outputs a lot of almost-black noise. SD1.5 ControlNets seem to be OK with this, but + # SDXL ControlNets are not - they need a cleaner map. 12 was experimentally determined to be a good threshold, + # eliminating all the noise while keeping the actual edges. Other approaches to thresholding may be better, + # for example stretching the contrast or removing noise. + detected_map[detected_map < 12] = 0 + + output = np_to_pil(detected_map) + + return output diff --git a/invokeai/backend/image_util/mediapipe_face/__init__.py b/invokeai/backend/image_util/mediapipe_face/__init__.py new file mode 100644 index 00000000000..da41425b433 --- /dev/null +++ b/invokeai/backend/image_util/mediapipe_face/__init__.py @@ -0,0 +1,15 @@ +# Adapted from https://github.com/huggingface/controlnet_aux + +from PIL import Image + +from invokeai.backend.image_util.mediapipe_face.mediapipe_face_common import generate_annotation +from invokeai.backend.image_util.util import np_to_pil, pil_to_np + + +def detect_faces(image: Image.Image, max_faces: int = 1, min_confidence: float = 0.5) -> Image.Image: + """Detects faces in an image using MediaPipe.""" + + np_img = pil_to_np(image) + detected_map = generate_annotation(np_img, max_faces, min_confidence) + detected_map_pil = np_to_pil(detected_map) + return detected_map_pil diff --git a/invokeai/backend/image_util/mediapipe_face/mediapipe_face_common.py b/invokeai/backend/image_util/mediapipe_face/mediapipe_face_common.py new file mode 100644 index 00000000000..4cf7a66cdc7 --- /dev/null +++ b/invokeai/backend/image_util/mediapipe_face/mediapipe_face_common.py @@ -0,0 +1,149 @@ +from typing import Mapping + +import mediapipe as mp +import numpy + +mp_drawing = mp.solutions.drawing_utils +mp_drawing_styles = mp.solutions.drawing_styles +mp_face_detection = mp.solutions.face_detection # Only for counting faces. +mp_face_mesh = mp.solutions.face_mesh +mp_face_connections = mp.solutions.face_mesh_connections.FACEMESH_TESSELATION +mp_hand_connections = mp.solutions.hands_connections.HAND_CONNECTIONS +mp_body_connections = mp.solutions.pose_connections.POSE_CONNECTIONS + +DrawingSpec = mp.solutions.drawing_styles.DrawingSpec +PoseLandmark = mp.solutions.drawing_styles.PoseLandmark + +min_face_size_pixels: int = 64 +f_thick = 2 +f_rad = 1 +right_iris_draw = DrawingSpec(color=(10, 200, 250), thickness=f_thick, circle_radius=f_rad) +right_eye_draw = DrawingSpec(color=(10, 200, 180), thickness=f_thick, circle_radius=f_rad) +right_eyebrow_draw = DrawingSpec(color=(10, 220, 180), thickness=f_thick, circle_radius=f_rad) +left_iris_draw = DrawingSpec(color=(250, 200, 10), thickness=f_thick, circle_radius=f_rad) +left_eye_draw = DrawingSpec(color=(180, 200, 10), thickness=f_thick, circle_radius=f_rad) +left_eyebrow_draw = DrawingSpec(color=(180, 220, 10), thickness=f_thick, circle_radius=f_rad) +mouth_draw = DrawingSpec(color=(10, 180, 10), thickness=f_thick, circle_radius=f_rad) +head_draw = DrawingSpec(color=(10, 200, 10), thickness=f_thick, circle_radius=f_rad) + +# mp_face_mesh.FACEMESH_CONTOURS has all the items we care about. +face_connection_spec = {} +for edge in mp_face_mesh.FACEMESH_FACE_OVAL: + face_connection_spec[edge] = head_draw +for edge in mp_face_mesh.FACEMESH_LEFT_EYE: + face_connection_spec[edge] = left_eye_draw +for edge in mp_face_mesh.FACEMESH_LEFT_EYEBROW: + face_connection_spec[edge] = left_eyebrow_draw +# for edge in mp_face_mesh.FACEMESH_LEFT_IRIS: +# face_connection_spec[edge] = left_iris_draw +for edge in mp_face_mesh.FACEMESH_RIGHT_EYE: + face_connection_spec[edge] = right_eye_draw +for edge in mp_face_mesh.FACEMESH_RIGHT_EYEBROW: + face_connection_spec[edge] = right_eyebrow_draw +# for edge in mp_face_mesh.FACEMESH_RIGHT_IRIS: +# face_connection_spec[edge] = right_iris_draw +for edge in mp_face_mesh.FACEMESH_LIPS: + face_connection_spec[edge] = mouth_draw +iris_landmark_spec = {468: right_iris_draw, 473: left_iris_draw} + + +def draw_pupils(image, landmark_list, drawing_spec, halfwidth: int = 2): + """We have a custom function to draw the pupils because the mp.draw_landmarks method requires a parameter for all + landmarks. Until our PR is merged into mediapipe, we need this separate method.""" + if len(image.shape) != 3: + raise ValueError("Input image must be H,W,C.") + image_rows, image_cols, image_channels = image.shape + if image_channels != 3: # BGR channels + raise ValueError("Input image must contain three channel bgr data.") + for idx, landmark in enumerate(landmark_list.landmark): + if (landmark.HasField("visibility") and landmark.visibility < 0.9) or ( + landmark.HasField("presence") and landmark.presence < 0.5 + ): + continue + if landmark.x >= 1.0 or landmark.x < 0 or landmark.y >= 1.0 or landmark.y < 0: + continue + image_x = int(image_cols * landmark.x) + image_y = int(image_rows * landmark.y) + draw_color = None + if isinstance(drawing_spec, Mapping): + if drawing_spec.get(idx) is None: + continue + else: + draw_color = drawing_spec[idx].color + elif isinstance(drawing_spec, DrawingSpec): + draw_color = drawing_spec.color + image[image_y - halfwidth : image_y + halfwidth, image_x - halfwidth : image_x + halfwidth, :] = draw_color + + +def reverse_channels(image): + """Given a numpy array in RGB form, convert to BGR. Will also convert from BGR to RGB.""" + # im[:,:,::-1] is a neat hack to convert BGR to RGB by reversing the indexing order. + # im[:,:,::[2,1,0]] would also work but makes a copy of the data. + return image[:, :, ::-1] + + +def generate_annotation(img_rgb, max_faces: int, min_confidence: float): + """ + Find up to 'max_faces' inside the provided input image. + If min_face_size_pixels is provided and nonzero it will be used to filter faces that occupy less than this many + pixels in the image. + """ + with mp_face_mesh.FaceMesh( + static_image_mode=True, + max_num_faces=max_faces, + refine_landmarks=True, + min_detection_confidence=min_confidence, + ) as facemesh: + img_height, img_width, img_channels = img_rgb.shape + assert img_channels == 3 + + results = facemesh.process(img_rgb).multi_face_landmarks + + if results is None: + print("No faces detected in controlnet image for Mediapipe face annotator.") + return numpy.zeros_like(img_rgb) + + # Filter faces that are too small + filtered_landmarks = [] + for lm in results: + landmarks = lm.landmark + face_rect = [ + landmarks[0].x, + landmarks[0].y, + landmarks[0].x, + landmarks[0].y, + ] # Left, up, right, down. + for i in range(len(landmarks)): + face_rect[0] = min(face_rect[0], landmarks[i].x) + face_rect[1] = min(face_rect[1], landmarks[i].y) + face_rect[2] = max(face_rect[2], landmarks[i].x) + face_rect[3] = max(face_rect[3], landmarks[i].y) + if min_face_size_pixels > 0: + face_width = abs(face_rect[2] - face_rect[0]) + face_height = abs(face_rect[3] - face_rect[1]) + face_width_pixels = face_width * img_width + face_height_pixels = face_height * img_height + face_size = min(face_width_pixels, face_height_pixels) + if face_size >= min_face_size_pixels: + filtered_landmarks.append(lm) + else: + filtered_landmarks.append(lm) + + # Annotations are drawn in BGR for some reason, but we don't need to flip a zero-filled image at the start. + empty = numpy.zeros_like(img_rgb) + + # Draw detected faces: + for face_landmarks in filtered_landmarks: + mp_drawing.draw_landmarks( + empty, + face_landmarks, + connections=face_connection_spec.keys(), + landmark_drawing_spec=None, + connection_drawing_spec=face_connection_spec, + ) + draw_pupils(empty, face_landmarks, iris_landmark_spec, 2) + + # Flip BGR back to RGB. + empty = reverse_channels(empty).copy() + + return empty diff --git a/invokeai/backend/image_util/mlsd/__init__.py b/invokeai/backend/image_util/mlsd/__init__.py new file mode 100644 index 00000000000..0423865be73 --- /dev/null +++ b/invokeai/backend/image_util/mlsd/__init__.py @@ -0,0 +1,66 @@ +# Adapted from https://github.com/huggingface/controlnet_aux + +import pathlib + +import cv2 +import huggingface_hub +import numpy as np +import torch +from PIL import Image + +from invokeai.backend.image_util.mlsd.models.mbv2_mlsd_large import MobileV2_MLSD_Large +from invokeai.backend.image_util.mlsd.utils import pred_lines +from invokeai.backend.image_util.util import np_to_pil, pil_to_np, resize_to_multiple + + +class MLSDDetector: + """Simple wrapper around a MLSD model for detecting edges as line segments in an image.""" + + hf_repo_id = "lllyasviel/ControlNet" + hf_filename = "annotator/ckpts/mlsd_large_512_fp32.pth" + + @classmethod + def get_model_url(cls) -> str: + """Get the URL to download the model from the Hugging Face Hub.""" + + return huggingface_hub.hf_hub_url(cls.hf_repo_id, cls.hf_filename) + + @classmethod + def load_model(cls, model_path: pathlib.Path) -> MobileV2_MLSD_Large: + """Load the model from a file.""" + + model = MobileV2_MLSD_Large() + model.load_state_dict(torch.load(model_path), strict=True) + model.eval() + return model + + def __init__(self, model: MobileV2_MLSD_Large) -> None: + self.model = model + + def to(self, device: torch.device): + self.model.to(device) + return self + + def run(self, image: Image.Image, score_threshold: float = 0.1, distance_threshold: float = 20.0) -> Image.Image: + """Processes an image and returns the detected edges.""" + + np_img = pil_to_np(image) + + height, width, _channels = np_img.shape + + # This model requires the input image to have a resolution that is a multiple of 64 + np_img = resize_to_multiple(np_img, 64) + img_output = np.zeros_like(np_img) + + with torch.no_grad(): + lines = pred_lines(np_img, self.model, [np_img.shape[0], np_img.shape[1]], score_threshold, distance_threshold) + for line in lines: + x_start, y_start, x_end, y_end = [int(val) for val in line] + cv2.line(img_output, (x_start, y_start), (x_end, y_end), [255, 255, 255], 1) + + detected_map = img_output[:, :, 0] + + # Back to the original size + output_image = cv2.resize(detected_map, (width, height), interpolation=cv2.INTER_LINEAR) + + return np_to_pil(output_image) diff --git a/invokeai/backend/image_util/mlsd/models/__init__.py b/invokeai/backend/image_util/mlsd/models/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/image_util/mlsd/models/mbv2_mlsd_large.py b/invokeai/backend/image_util/mlsd/models/mbv2_mlsd_large.py new file mode 100644 index 00000000000..7d21284ef63 --- /dev/null +++ b/invokeai/backend/image_util/mlsd/models/mbv2_mlsd_large.py @@ -0,0 +1,290 @@ +import torch +import torch.nn as nn +import torch.utils.model_zoo as model_zoo +from torch.nn import functional as F + + +class BlockTypeA(nn.Module): + def __init__(self, in_c1, in_c2, out_c1, out_c2, upscale = True): + super(BlockTypeA, self).__init__() + self.conv1 = nn.Sequential( + nn.Conv2d(in_c2, out_c2, kernel_size=1), + nn.BatchNorm2d(out_c2), + nn.ReLU(inplace=True) + ) + self.conv2 = nn.Sequential( + nn.Conv2d(in_c1, out_c1, kernel_size=1), + nn.BatchNorm2d(out_c1), + nn.ReLU(inplace=True) + ) + self.upscale = upscale + + def forward(self, a, b): + b = self.conv1(b) + a = self.conv2(a) + if self.upscale: + b = F.interpolate(b, scale_factor=2.0, mode='bilinear', align_corners=True) + return torch.cat((a, b), dim=1) + + +class BlockTypeB(nn.Module): + def __init__(self, in_c, out_c): + super(BlockTypeB, self).__init__() + self.conv1 = nn.Sequential( + nn.Conv2d(in_c, in_c, kernel_size=3, padding=1), + nn.BatchNorm2d(in_c), + nn.ReLU() + ) + self.conv2 = nn.Sequential( + nn.Conv2d(in_c, out_c, kernel_size=3, padding=1), + nn.BatchNorm2d(out_c), + nn.ReLU() + ) + + def forward(self, x): + x = self.conv1(x) + x + x = self.conv2(x) + return x + +class BlockTypeC(nn.Module): + def __init__(self, in_c, out_c): + super(BlockTypeC, self).__init__() + self.conv1 = nn.Sequential( + nn.Conv2d(in_c, in_c, kernel_size=3, padding=5, dilation=5), + nn.BatchNorm2d(in_c), + nn.ReLU() + ) + self.conv2 = nn.Sequential( + nn.Conv2d(in_c, in_c, kernel_size=3, padding=1), + nn.BatchNorm2d(in_c), + nn.ReLU() + ) + self.conv3 = nn.Conv2d(in_c, out_c, kernel_size=1) + + def forward(self, x): + x = self.conv1(x) + x = self.conv2(x) + x = self.conv3(x) + return x + +def _make_divisible(v, divisor, min_value=None): + """ + This function is taken from the original tf repo. + It ensures that all layers have a channel number that is divisible by 8 + It can be seen here: + https://github.com/tensorflow/models/blob/master/research/slim/nets/mobilenet/mobilenet.py + :param v: + :param divisor: + :param min_value: + :return: + """ + if min_value is None: + min_value = divisor + new_v = max(min_value, int(v + divisor / 2) // divisor * divisor) + # Make sure that round down does not go down by more than 10%. + if new_v < 0.9 * v: + new_v += divisor + return new_v + + +class ConvBNReLU(nn.Sequential): + def __init__(self, in_planes, out_planes, kernel_size=3, stride=1, groups=1): + self.channel_pad = out_planes - in_planes + self.stride = stride + #padding = (kernel_size - 1) // 2 + + # TFLite uses slightly different padding than PyTorch + if stride == 2: + padding = 0 + else: + padding = (kernel_size - 1) // 2 + + super(ConvBNReLU, self).__init__( + nn.Conv2d(in_planes, out_planes, kernel_size, stride, padding, groups=groups, bias=False), + nn.BatchNorm2d(out_planes), + nn.ReLU6(inplace=True) + ) + self.max_pool = nn.MaxPool2d(kernel_size=stride, stride=stride) + + + def forward(self, x): + # TFLite uses different padding + if self.stride == 2: + x = F.pad(x, (0, 1, 0, 1), "constant", 0) + #print(x.shape) + + for module in self: + if not isinstance(module, nn.MaxPool2d): + x = module(x) + return x + + +class InvertedResidual(nn.Module): + def __init__(self, inp, oup, stride, expand_ratio): + super(InvertedResidual, self).__init__() + self.stride = stride + assert stride in [1, 2] + + hidden_dim = int(round(inp * expand_ratio)) + self.use_res_connect = self.stride == 1 and inp == oup + + layers = [] + if expand_ratio != 1: + # pw + layers.append(ConvBNReLU(inp, hidden_dim, kernel_size=1)) + layers.extend([ + # dw + ConvBNReLU(hidden_dim, hidden_dim, stride=stride, groups=hidden_dim), + # pw-linear + nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False), + nn.BatchNorm2d(oup), + ]) + self.conv = nn.Sequential(*layers) + + def forward(self, x): + if self.use_res_connect: + return x + self.conv(x) + else: + return self.conv(x) + + +class MobileNetV2(nn.Module): + def __init__(self, pretrained=True): + """ + MobileNet V2 main class + Args: + num_classes (int): Number of classes + width_mult (float): Width multiplier - adjusts number of channels in each layer by this amount + inverted_residual_setting: Network structure + round_nearest (int): Round the number of channels in each layer to be a multiple of this number + Set to 1 to turn off rounding + block: Module specifying inverted residual building block for mobilenet + """ + super(MobileNetV2, self).__init__() + + block = InvertedResidual + input_channel = 32 + last_channel = 1280 + width_mult = 1.0 + round_nearest = 8 + + inverted_residual_setting = [ + # t, c, n, s + [1, 16, 1, 1], + [6, 24, 2, 2], + [6, 32, 3, 2], + [6, 64, 4, 2], + [6, 96, 3, 1], + #[6, 160, 3, 2], + #[6, 320, 1, 1], + ] + + # only check the first element, assuming user knows t,c,n,s are required + if len(inverted_residual_setting) == 0 or len(inverted_residual_setting[0]) != 4: + raise ValueError("inverted_residual_setting should be non-empty " + "or a 4-element list, got {}".format(inverted_residual_setting)) + + # building first layer + input_channel = _make_divisible(input_channel * width_mult, round_nearest) + self.last_channel = _make_divisible(last_channel * max(1.0, width_mult), round_nearest) + features = [ConvBNReLU(4, input_channel, stride=2)] + # building inverted residual blocks + for t, c, n, s in inverted_residual_setting: + output_channel = _make_divisible(c * width_mult, round_nearest) + for i in range(n): + stride = s if i == 0 else 1 + features.append(block(input_channel, output_channel, stride, expand_ratio=t)) + input_channel = output_channel + + self.features = nn.Sequential(*features) + self.fpn_selected = [1, 3, 6, 10, 13] + # weight initialization + for m in self.modules(): + if isinstance(m, nn.Conv2d): + nn.init.kaiming_normal_(m.weight, mode='fan_out') + if m.bias is not None: + nn.init.zeros_(m.bias) + elif isinstance(m, nn.BatchNorm2d): + nn.init.ones_(m.weight) + nn.init.zeros_(m.bias) + elif isinstance(m, nn.Linear): + nn.init.normal_(m.weight, 0, 0.01) + nn.init.zeros_(m.bias) + if pretrained: + self._load_pretrained_model() + + def _forward_impl(self, x): + # This exists since TorchScript doesn't support inheritance, so the superclass method + # (this one) needs to have a name other than `forward` that can be accessed in a subclass + fpn_features = [] + for i, f in enumerate(self.features): + if i > self.fpn_selected[-1]: + break + x = f(x) + if i in self.fpn_selected: + fpn_features.append(x) + + c1, c2, c3, c4, c5 = fpn_features + return c1, c2, c3, c4, c5 + + + def forward(self, x): + return self._forward_impl(x) + + def _load_pretrained_model(self): + pretrain_dict = model_zoo.load_url('https://download.pytorch.org/models/mobilenet_v2-b0353104.pth') + model_dict = {} + state_dict = self.state_dict() + for k, v in pretrain_dict.items(): + if k in state_dict: + model_dict[k] = v + state_dict.update(model_dict) + self.load_state_dict(state_dict) + + +class MobileV2_MLSD_Large(nn.Module): + def __init__(self): + super(MobileV2_MLSD_Large, self).__init__() + + self.backbone = MobileNetV2(pretrained=False) + ## A, B + self.block15 = BlockTypeA(in_c1= 64, in_c2= 96, + out_c1= 64, out_c2=64, + upscale=False) + self.block16 = BlockTypeB(128, 64) + + ## A, B + self.block17 = BlockTypeA(in_c1 = 32, in_c2 = 64, + out_c1= 64, out_c2= 64) + self.block18 = BlockTypeB(128, 64) + + ## A, B + self.block19 = BlockTypeA(in_c1=24, in_c2=64, + out_c1=64, out_c2=64) + self.block20 = BlockTypeB(128, 64) + + ## A, B, C + self.block21 = BlockTypeA(in_c1=16, in_c2=64, + out_c1=64, out_c2=64) + self.block22 = BlockTypeB(128, 64) + + self.block23 = BlockTypeC(64, 16) + + def forward(self, x): + c1, c2, c3, c4, c5 = self.backbone(x) + + x = self.block15(c4, c5) + x = self.block16(x) + + x = self.block17(c3, x) + x = self.block18(x) + + x = self.block19(c2, x) + x = self.block20(x) + + x = self.block21(c1, x) + x = self.block22(x) + x = self.block23(x) + x = x[:, 7:, :, :] + + return x diff --git a/invokeai/backend/image_util/mlsd/models/mbv2_mlsd_tiny.py b/invokeai/backend/image_util/mlsd/models/mbv2_mlsd_tiny.py new file mode 100644 index 00000000000..5c1f94af648 --- /dev/null +++ b/invokeai/backend/image_util/mlsd/models/mbv2_mlsd_tiny.py @@ -0,0 +1,273 @@ +import torch +import torch.nn as nn +import torch.utils.model_zoo as model_zoo +from torch.nn import functional as F + + +class BlockTypeA(nn.Module): + def __init__(self, in_c1, in_c2, out_c1, out_c2, upscale = True): + super(BlockTypeA, self).__init__() + self.conv1 = nn.Sequential( + nn.Conv2d(in_c2, out_c2, kernel_size=1), + nn.BatchNorm2d(out_c2), + nn.ReLU(inplace=True) + ) + self.conv2 = nn.Sequential( + nn.Conv2d(in_c1, out_c1, kernel_size=1), + nn.BatchNorm2d(out_c1), + nn.ReLU(inplace=True) + ) + self.upscale = upscale + + def forward(self, a, b): + b = self.conv1(b) + a = self.conv2(a) + b = F.interpolate(b, scale_factor=2.0, mode='bilinear', align_corners=True) + return torch.cat((a, b), dim=1) + + +class BlockTypeB(nn.Module): + def __init__(self, in_c, out_c): + super(BlockTypeB, self).__init__() + self.conv1 = nn.Sequential( + nn.Conv2d(in_c, in_c, kernel_size=3, padding=1), + nn.BatchNorm2d(in_c), + nn.ReLU() + ) + self.conv2 = nn.Sequential( + nn.Conv2d(in_c, out_c, kernel_size=3, padding=1), + nn.BatchNorm2d(out_c), + nn.ReLU() + ) + + def forward(self, x): + x = self.conv1(x) + x + x = self.conv2(x) + return x + +class BlockTypeC(nn.Module): + def __init__(self, in_c, out_c): + super(BlockTypeC, self).__init__() + self.conv1 = nn.Sequential( + nn.Conv2d(in_c, in_c, kernel_size=3, padding=5, dilation=5), + nn.BatchNorm2d(in_c), + nn.ReLU() + ) + self.conv2 = nn.Sequential( + nn.Conv2d(in_c, in_c, kernel_size=3, padding=1), + nn.BatchNorm2d(in_c), + nn.ReLU() + ) + self.conv3 = nn.Conv2d(in_c, out_c, kernel_size=1) + + def forward(self, x): + x = self.conv1(x) + x = self.conv2(x) + x = self.conv3(x) + return x + +def _make_divisible(v, divisor, min_value=None): + """ + This function is taken from the original tf repo. + It ensures that all layers have a channel number that is divisible by 8 + It can be seen here: + https://github.com/tensorflow/models/blob/master/research/slim/nets/mobilenet/mobilenet.py + :param v: + :param divisor: + :param min_value: + :return: + """ + if min_value is None: + min_value = divisor + new_v = max(min_value, int(v + divisor / 2) // divisor * divisor) + # Make sure that round down does not go down by more than 10%. + if new_v < 0.9 * v: + new_v += divisor + return new_v + + +class ConvBNReLU(nn.Sequential): + def __init__(self, in_planes, out_planes, kernel_size=3, stride=1, groups=1): + self.channel_pad = out_planes - in_planes + self.stride = stride + #padding = (kernel_size - 1) // 2 + + # TFLite uses slightly different padding than PyTorch + if stride == 2: + padding = 0 + else: + padding = (kernel_size - 1) // 2 + + super(ConvBNReLU, self).__init__( + nn.Conv2d(in_planes, out_planes, kernel_size, stride, padding, groups=groups, bias=False), + nn.BatchNorm2d(out_planes), + nn.ReLU6(inplace=True) + ) + self.max_pool = nn.MaxPool2d(kernel_size=stride, stride=stride) + + + def forward(self, x): + # TFLite uses different padding + if self.stride == 2: + x = F.pad(x, (0, 1, 0, 1), "constant", 0) + #print(x.shape) + + for module in self: + if not isinstance(module, nn.MaxPool2d): + x = module(x) + return x + + +class InvertedResidual(nn.Module): + def __init__(self, inp, oup, stride, expand_ratio): + super(InvertedResidual, self).__init__() + self.stride = stride + assert stride in [1, 2] + + hidden_dim = int(round(inp * expand_ratio)) + self.use_res_connect = self.stride == 1 and inp == oup + + layers = [] + if expand_ratio != 1: + # pw + layers.append(ConvBNReLU(inp, hidden_dim, kernel_size=1)) + layers.extend([ + # dw + ConvBNReLU(hidden_dim, hidden_dim, stride=stride, groups=hidden_dim), + # pw-linear + nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False), + nn.BatchNorm2d(oup), + ]) + self.conv = nn.Sequential(*layers) + + def forward(self, x): + if self.use_res_connect: + return x + self.conv(x) + else: + return self.conv(x) + + +class MobileNetV2(nn.Module): + def __init__(self, pretrained=True): + """ + MobileNet V2 main class + Args: + num_classes (int): Number of classes + width_mult (float): Width multiplier - adjusts number of channels in each layer by this amount + inverted_residual_setting: Network structure + round_nearest (int): Round the number of channels in each layer to be a multiple of this number + Set to 1 to turn off rounding + block: Module specifying inverted residual building block for mobilenet + """ + super(MobileNetV2, self).__init__() + + block = InvertedResidual + input_channel = 32 + last_channel = 1280 + width_mult = 1.0 + round_nearest = 8 + + inverted_residual_setting = [ + # t, c, n, s + [1, 16, 1, 1], + [6, 24, 2, 2], + [6, 32, 3, 2], + [6, 64, 4, 2], + #[6, 96, 3, 1], + #[6, 160, 3, 2], + #[6, 320, 1, 1], + ] + + # only check the first element, assuming user knows t,c,n,s are required + if len(inverted_residual_setting) == 0 or len(inverted_residual_setting[0]) != 4: + raise ValueError("inverted_residual_setting should be non-empty " + "or a 4-element list, got {}".format(inverted_residual_setting)) + + # building first layer + input_channel = _make_divisible(input_channel * width_mult, round_nearest) + self.last_channel = _make_divisible(last_channel * max(1.0, width_mult), round_nearest) + features = [ConvBNReLU(4, input_channel, stride=2)] + # building inverted residual blocks + for t, c, n, s in inverted_residual_setting: + output_channel = _make_divisible(c * width_mult, round_nearest) + for i in range(n): + stride = s if i == 0 else 1 + features.append(block(input_channel, output_channel, stride, expand_ratio=t)) + input_channel = output_channel + self.features = nn.Sequential(*features) + + self.fpn_selected = [3, 6, 10] + # weight initialization + for m in self.modules(): + if isinstance(m, nn.Conv2d): + nn.init.kaiming_normal_(m.weight, mode='fan_out') + if m.bias is not None: + nn.init.zeros_(m.bias) + elif isinstance(m, nn.BatchNorm2d): + nn.init.ones_(m.weight) + nn.init.zeros_(m.bias) + elif isinstance(m, nn.Linear): + nn.init.normal_(m.weight, 0, 0.01) + nn.init.zeros_(m.bias) + + #if pretrained: + # self._load_pretrained_model() + + def _forward_impl(self, x): + # This exists since TorchScript doesn't support inheritance, so the superclass method + # (this one) needs to have a name other than `forward` that can be accessed in a subclass + fpn_features = [] + for i, f in enumerate(self.features): + if i > self.fpn_selected[-1]: + break + x = f(x) + if i in self.fpn_selected: + fpn_features.append(x) + + c2, c3, c4 = fpn_features + return c2, c3, c4 + + + def forward(self, x): + return self._forward_impl(x) + + def _load_pretrained_model(self): + pretrain_dict = model_zoo.load_url('https://download.pytorch.org/models/mobilenet_v2-b0353104.pth') + model_dict = {} + state_dict = self.state_dict() + for k, v in pretrain_dict.items(): + if k in state_dict: + model_dict[k] = v + state_dict.update(model_dict) + self.load_state_dict(state_dict) + + +class MobileV2_MLSD_Tiny(nn.Module): + def __init__(self): + super(MobileV2_MLSD_Tiny, self).__init__() + + self.backbone = MobileNetV2(pretrained=True) + + self.block12 = BlockTypeA(in_c1= 32, in_c2= 64, + out_c1= 64, out_c2=64) + self.block13 = BlockTypeB(128, 64) + + self.block14 = BlockTypeA(in_c1 = 24, in_c2 = 64, + out_c1= 32, out_c2= 32) + self.block15 = BlockTypeB(64, 64) + + self.block16 = BlockTypeC(64, 16) + + def forward(self, x): + c2, c3, c4 = self.backbone(x) + + x = self.block12(c3, c4) + x = self.block13(x) + x = self.block14(c2, x) + x = self.block15(x) + x = self.block16(x) + x = x[:, 7:, :, :] + #print(x.shape) + x = F.interpolate(x, scale_factor=2.0, mode='bilinear', align_corners=True) + + return x diff --git a/invokeai/backend/image_util/mlsd/utils.py b/invokeai/backend/image_util/mlsd/utils.py new file mode 100644 index 00000000000..ee51e0f615d --- /dev/null +++ b/invokeai/backend/image_util/mlsd/utils.py @@ -0,0 +1,589 @@ +''' +modified by lihaoweicv +pytorch version +''' + +''' +M-LSD +Copyright 2021-present NAVER Corp. +Apache License v2.0 +''' + +import cv2 +import numpy as np +import torch +from torch.nn import functional as F + +from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device + + +def deccode_output_score_and_ptss(tpMap, topk_n = 200, ksize = 5): + ''' + tpMap: + center: tpMap[1, 0, :, :] + displacement: tpMap[1, 1:5, :, :] + ''' + b, c, h, w = tpMap.shape + assert b==1, 'only support bsize==1' + displacement = tpMap[:, 1:5, :, :][0] + center = tpMap[:, 0, :, :] + heat = torch.sigmoid(center) + hmax = F.max_pool2d( heat, (ksize, ksize), stride=1, padding=(ksize-1)//2) + keep = (hmax == heat).float() + heat = heat * keep + heat = heat.reshape(-1, ) + + scores, indices = torch.topk(heat, topk_n, dim=-1, largest=True) + yy = torch.floor_divide(indices, w).unsqueeze(-1) + xx = torch.fmod(indices, w).unsqueeze(-1) + ptss = torch.cat((yy, xx),dim=-1) + + ptss = ptss.detach().cpu().numpy() + scores = scores.detach().cpu().numpy() + displacement = displacement.detach().cpu().numpy() + displacement = displacement.transpose((1,2,0)) + return ptss, scores, displacement + + +def pred_lines(image, model, + input_shape=[512, 512], + score_thr=0.10, + dist_thr=20.0): + h, w, _ = image.shape + + device = get_effective_device(model) + h_ratio, w_ratio = [h / input_shape[0], w / input_shape[1]] + + resized_image = np.concatenate([cv2.resize(image, (input_shape[1], input_shape[0]), interpolation=cv2.INTER_AREA), + np.ones([input_shape[0], input_shape[1], 1])], axis=-1) + + resized_image = resized_image.transpose((2,0,1)) + batch_image = np.expand_dims(resized_image, axis=0).astype('float32') + batch_image = (batch_image / 127.5) - 1.0 + + batch_image = torch.from_numpy(batch_image).float() + batch_image = batch_image.to(device) + outputs = model(batch_image) + pts, pts_score, vmap = deccode_output_score_and_ptss(outputs, 200, 3) + start = vmap[:, :, :2] + end = vmap[:, :, 2:] + dist_map = np.sqrt(np.sum((start - end) ** 2, axis=-1)) + + segments_list = [] + for center, score in zip(pts, pts_score, strict=False): + y, x = center + distance = dist_map[y, x] + if score > score_thr and distance > dist_thr: + disp_x_start, disp_y_start, disp_x_end, disp_y_end = vmap[y, x, :] + x_start = x + disp_x_start + y_start = y + disp_y_start + x_end = x + disp_x_end + y_end = y + disp_y_end + segments_list.append([x_start, y_start, x_end, y_end]) + + if segments_list: + lines = 2 * np.array(segments_list) # 256 > 512 + lines[:, 0] = lines[:, 0] * w_ratio + lines[:, 1] = lines[:, 1] * h_ratio + lines[:, 2] = lines[:, 2] * w_ratio + lines[:, 3] = lines[:, 3] * h_ratio + else: + # No segments detected - return empty array + lines = np.array([]) + + return lines + + +def pred_squares(image, + model, + input_shape=[512, 512], + params={'score': 0.06, + 'outside_ratio': 0.28, + 'inside_ratio': 0.45, + 'w_overlap': 0.0, + 'w_degree': 1.95, + 'w_length': 0.0, + 'w_area': 1.86, + 'w_center': 0.14}): + ''' + shape = [height, width] + ''' + h, w, _ = image.shape + original_shape = [h, w] + device = get_effective_device(model) + + resized_image = np.concatenate([cv2.resize(image, (input_shape[0], input_shape[1]), interpolation=cv2.INTER_AREA), + np.ones([input_shape[0], input_shape[1], 1])], axis=-1) + resized_image = resized_image.transpose((2, 0, 1)) + batch_image = np.expand_dims(resized_image, axis=0).astype('float32') + batch_image = (batch_image / 127.5) - 1.0 + + batch_image = torch.from_numpy(batch_image).float().to(device) + outputs = model(batch_image) + + pts, pts_score, vmap = deccode_output_score_and_ptss(outputs, 200, 3) + start = vmap[:, :, :2] # (x, y) + end = vmap[:, :, 2:] # (x, y) + dist_map = np.sqrt(np.sum((start - end) ** 2, axis=-1)) + + junc_list = [] + segments_list = [] + for junc, score in zip(pts, pts_score, strict=False): + y, x = junc + distance = dist_map[y, x] + if score > params['score'] and distance > 20.0: + junc_list.append([x, y]) + disp_x_start, disp_y_start, disp_x_end, disp_y_end = vmap[y, x, :] + d_arrow = 1.0 + x_start = x + d_arrow * disp_x_start + y_start = y + d_arrow * disp_y_start + x_end = x + d_arrow * disp_x_end + y_end = y + d_arrow * disp_y_end + segments_list.append([x_start, y_start, x_end, y_end]) + + segments = np.array(segments_list) + + ####### post processing for squares + # 1. get unique lines + point = np.array([[0, 0]]) + point = point[0] + start = segments[:, :2] + end = segments[:, 2:] + diff = start - end + a = diff[:, 1] + b = -diff[:, 0] + c = a * start[:, 0] + b * start[:, 1] + + d = np.abs(a * point[0] + b * point[1] - c) / np.sqrt(a ** 2 + b ** 2 + 1e-10) + theta = np.arctan2(diff[:, 0], diff[:, 1]) * 180 / np.pi + theta[theta < 0.0] += 180 + hough = np.concatenate([d[:, None], theta[:, None]], axis=-1) + + d_quant = 1 + theta_quant = 2 + hough[:, 0] //= d_quant + hough[:, 1] //= theta_quant + _, indices, counts = np.unique(hough, axis=0, return_index=True, return_counts=True) + + acc_map = np.zeros([512 // d_quant + 1, 360 // theta_quant + 1], dtype='float32') + idx_map = np.zeros([512 // d_quant + 1, 360 // theta_quant + 1], dtype='int32') - 1 + yx_indices = hough[indices, :].astype('int32') + acc_map[yx_indices[:, 0], yx_indices[:, 1]] = counts + idx_map[yx_indices[:, 0], yx_indices[:, 1]] = indices + + acc_map_np = acc_map + # acc_map = acc_map[None, :, :, None] + # + # ### fast suppression using tensorflow op + # acc_map = tf.constant(acc_map, dtype=tf.float32) + # max_acc_map = tf.keras.layers.MaxPool2D(pool_size=(5, 5), strides=1, padding='same')(acc_map) + # acc_map = acc_map * tf.cast(tf.math.equal(acc_map, max_acc_map), tf.float32) + # flatten_acc_map = tf.reshape(acc_map, [1, -1]) + # topk_values, topk_indices = tf.math.top_k(flatten_acc_map, k=len(pts)) + # _, h, w, _ = acc_map.shape + # y = tf.expand_dims(topk_indices // w, axis=-1) + # x = tf.expand_dims(topk_indices % w, axis=-1) + # yx = tf.concat([y, x], axis=-1) + + ### fast suppression using pytorch op + acc_map = torch.from_numpy(acc_map_np).unsqueeze(0).unsqueeze(0) + _,_, h, w = acc_map.shape + max_acc_map = F.max_pool2d(acc_map,kernel_size=5, stride=1, padding=2) + acc_map = acc_map * ( (acc_map == max_acc_map).float() ) + flatten_acc_map = acc_map.reshape([-1, ]) + + scores, indices = torch.topk(flatten_acc_map, len(pts), dim=-1, largest=True) + yy = torch.div(indices, w, rounding_mode='floor').unsqueeze(-1) + xx = torch.fmod(indices, w).unsqueeze(-1) + yx = torch.cat((yy, xx), dim=-1) + + yx = yx.detach().cpu().numpy() + + topk_values = scores.detach().cpu().numpy() + indices = idx_map[yx[:, 0], yx[:, 1]] + basis = 5 // 2 + + merged_segments = [] + for yx_pt, max_indice, value in zip(yx, indices, topk_values, strict=False): + y, x = yx_pt + if max_indice == -1 or value == 0: + continue + segment_list = [] + for y_offset in range(-basis, basis + 1): + for x_offset in range(-basis, basis + 1): + indice = idx_map[y + y_offset, x + x_offset] + cnt = int(acc_map_np[y + y_offset, x + x_offset]) + if indice != -1: + segment_list.append(segments[indice]) + if cnt > 1: + check_cnt = 1 + current_hough = hough[indice] + for new_indice, new_hough in enumerate(hough): + if (current_hough == new_hough).all() and indice != new_indice: + segment_list.append(segments[new_indice]) + check_cnt += 1 + if check_cnt == cnt: + break + group_segments = np.array(segment_list).reshape([-1, 2]) + sorted_group_segments = np.sort(group_segments, axis=0) + x_min, y_min = sorted_group_segments[0, :] + x_max, y_max = sorted_group_segments[-1, :] + + deg = theta[max_indice] + if deg >= 90: + merged_segments.append([x_min, y_max, x_max, y_min]) + else: + merged_segments.append([x_min, y_min, x_max, y_max]) + + # 2. get intersections + new_segments = np.array(merged_segments) # (x1, y1, x2, y2) + start = new_segments[:, :2] # (x1, y1) + end = new_segments[:, 2:] # (x2, y2) + new_centers = (start + end) / 2.0 + diff = start - end + dist_segments = np.sqrt(np.sum(diff ** 2, axis=-1)) + + # ax + by = c + a = diff[:, 1] + b = -diff[:, 0] + c = a * start[:, 0] + b * start[:, 1] + pre_det = a[:, None] * b[None, :] + det = pre_det - np.transpose(pre_det) + + pre_inter_y = a[:, None] * c[None, :] + inter_y = (pre_inter_y - np.transpose(pre_inter_y)) / (det + 1e-10) + pre_inter_x = c[:, None] * b[None, :] + inter_x = (pre_inter_x - np.transpose(pre_inter_x)) / (det + 1e-10) + inter_pts = np.concatenate([inter_x[:, :, None], inter_y[:, :, None]], axis=-1).astype('int32') + + # 3. get corner information + # 3.1 get distance + ''' + dist_segments: + | dist(0), dist(1), dist(2), ...| + dist_inter_to_segment1: + | dist(inter,0), dist(inter,0), dist(inter,0), ... | + | dist(inter,1), dist(inter,1), dist(inter,1), ... | + ... + dist_inter_to_semgnet2: + | dist(inter,0), dist(inter,1), dist(inter,2), ... | + | dist(inter,0), dist(inter,1), dist(inter,2), ... | + ... + ''' + + dist_inter_to_segment1_start = np.sqrt( + np.sum(((inter_pts - start[:, None, :]) ** 2), axis=-1, keepdims=True)) # [n_batch, n_batch, 1] + dist_inter_to_segment1_end = np.sqrt( + np.sum(((inter_pts - end[:, None, :]) ** 2), axis=-1, keepdims=True)) # [n_batch, n_batch, 1] + dist_inter_to_segment2_start = np.sqrt( + np.sum(((inter_pts - start[None, :, :]) ** 2), axis=-1, keepdims=True)) # [n_batch, n_batch, 1] + dist_inter_to_segment2_end = np.sqrt( + np.sum(((inter_pts - end[None, :, :]) ** 2), axis=-1, keepdims=True)) # [n_batch, n_batch, 1] + + # sort ascending + dist_inter_to_segment1 = np.sort( + np.concatenate([dist_inter_to_segment1_start, dist_inter_to_segment1_end], axis=-1), + axis=-1) # [n_batch, n_batch, 2] + dist_inter_to_segment2 = np.sort( + np.concatenate([dist_inter_to_segment2_start, dist_inter_to_segment2_end], axis=-1), + axis=-1) # [n_batch, n_batch, 2] + + # 3.2 get degree + inter_to_start = new_centers[:, None, :] - inter_pts + deg_inter_to_start = np.arctan2(inter_to_start[:, :, 1], inter_to_start[:, :, 0]) * 180 / np.pi + deg_inter_to_start[deg_inter_to_start < 0.0] += 360 + inter_to_end = new_centers[None, :, :] - inter_pts + deg_inter_to_end = np.arctan2(inter_to_end[:, :, 1], inter_to_end[:, :, 0]) * 180 / np.pi + deg_inter_to_end[deg_inter_to_end < 0.0] += 360 + + ''' + B -- G + | | + C -- R + B : blue / G: green / C: cyan / R: red + + 0 -- 1 + | | + 3 -- 2 + ''' + # rename variables + deg1_map, deg2_map = deg_inter_to_start, deg_inter_to_end + # sort deg ascending + deg_sort = np.sort(np.concatenate([deg1_map[:, :, None], deg2_map[:, :, None]], axis=-1), axis=-1) + + deg_diff_map = np.abs(deg1_map - deg2_map) + # we only consider the smallest degree of intersect + deg_diff_map[deg_diff_map > 180] = 360 - deg_diff_map[deg_diff_map > 180] + + # define available degree range + deg_range = [60, 120] + + corner_dict = {corner_info: [] for corner_info in range(4)} + inter_points = [] + for i in range(inter_pts.shape[0]): + for j in range(i + 1, inter_pts.shape[1]): + # i, j > line index, always i < j + x, y = inter_pts[i, j, :] + deg1, deg2 = deg_sort[i, j, :] + deg_diff = deg_diff_map[i, j] + + check_degree = deg_diff > deg_range[0] and deg_diff < deg_range[1] + + outside_ratio = params['outside_ratio'] # over ratio >>> drop it! + inside_ratio = params['inside_ratio'] # over ratio >>> drop it! + check_distance = ((dist_inter_to_segment1[i, j, 1] >= dist_segments[i] and \ + dist_inter_to_segment1[i, j, 0] <= dist_segments[i] * outside_ratio) or \ + (dist_inter_to_segment1[i, j, 1] <= dist_segments[i] and \ + dist_inter_to_segment1[i, j, 0] <= dist_segments[i] * inside_ratio)) and \ + ((dist_inter_to_segment2[i, j, 1] >= dist_segments[j] and \ + dist_inter_to_segment2[i, j, 0] <= dist_segments[j] * outside_ratio) or \ + (dist_inter_to_segment2[i, j, 1] <= dist_segments[j] and \ + dist_inter_to_segment2[i, j, 0] <= dist_segments[j] * inside_ratio)) + + if check_degree and check_distance: + corner_info = None + + if (deg1 >= 0 and deg1 <= 45 and deg2 >= 45 and deg2 <= 120) or \ + (deg2 >= 315 and deg1 >= 45 and deg1 <= 120): + corner_info, color_info = 0, 'blue' + elif (deg1 >= 45 and deg1 <= 125 and deg2 >= 125 and deg2 <= 225): + corner_info, color_info = 1, 'green' + elif (deg1 >= 125 and deg1 <= 225 and deg2 >= 225 and deg2 <= 315): + corner_info, color_info = 2, 'black' + elif (deg1 >= 0 and deg1 <= 45 and deg2 >= 225 and deg2 <= 315) or \ + (deg2 >= 315 and deg1 >= 225 and deg1 <= 315): + corner_info, color_info = 3, 'cyan' + else: + corner_info, color_info = 4, 'red' # we don't use it + continue + + corner_dict[corner_info].append([x, y, i, j]) + inter_points.append([x, y]) + + square_list = [] + connect_list = [] + segments_list = [] + for corner0 in corner_dict[0]: + for corner1 in corner_dict[1]: + connect01 = False + for corner0_line in corner0[2:]: + if corner0_line in corner1[2:]: + connect01 = True + break + if connect01: + for corner2 in corner_dict[2]: + connect12 = False + for corner1_line in corner1[2:]: + if corner1_line in corner2[2:]: + connect12 = True + break + if connect12: + for corner3 in corner_dict[3]: + connect23 = False + for corner2_line in corner2[2:]: + if corner2_line in corner3[2:]: + connect23 = True + break + if connect23: + for corner3_line in corner3[2:]: + if corner3_line in corner0[2:]: + # SQUARE!!! + ''' + 0 -- 1 + | | + 3 -- 2 + square_list: + order: 0 > 1 > 2 > 3 + | x0, y0, x1, y1, x2, y2, x3, y3 | + | x0, y0, x1, y1, x2, y2, x3, y3 | + ... + connect_list: + order: 01 > 12 > 23 > 30 + | line_idx01, line_idx12, line_idx23, line_idx30 | + | line_idx01, line_idx12, line_idx23, line_idx30 | + ... + segments_list: + order: 0 > 1 > 2 > 3 + | line_idx0_i, line_idx0_j, line_idx1_i, line_idx1_j, line_idx2_i, line_idx2_j, line_idx3_i, line_idx3_j | + | line_idx0_i, line_idx0_j, line_idx1_i, line_idx1_j, line_idx2_i, line_idx2_j, line_idx3_i, line_idx3_j | + ... + ''' + square_list.append(corner0[:2] + corner1[:2] + corner2[:2] + corner3[:2]) + connect_list.append([corner0_line, corner1_line, corner2_line, corner3_line]) + segments_list.append(corner0[2:] + corner1[2:] + corner2[2:] + corner3[2:]) + + def check_outside_inside(segments_info, connect_idx): + # return 'outside or inside', min distance, cover_param, peri_param + if connect_idx == segments_info[0]: + check_dist_mat = dist_inter_to_segment1 + else: + check_dist_mat = dist_inter_to_segment2 + + i, j = segments_info + min_dist, max_dist = check_dist_mat[i, j, :] + connect_dist = dist_segments[connect_idx] + if max_dist > connect_dist: + return 'outside', min_dist, 0, 1 + else: + return 'inside', min_dist, -1, -1 + + top_square = None + + try: + map_size = input_shape[0] / 2 + squares = np.array(square_list).reshape([-1, 4, 2]) + score_array = [] + connect_array = np.array(connect_list) + segments_array = np.array(segments_list).reshape([-1, 4, 2]) + + # get degree of corners: + squares_rollup = np.roll(squares, 1, axis=1) + squares_rolldown = np.roll(squares, -1, axis=1) + vec1 = squares_rollup - squares + normalized_vec1 = vec1 / (np.linalg.norm(vec1, axis=-1, keepdims=True) + 1e-10) + vec2 = squares_rolldown - squares + normalized_vec2 = vec2 / (np.linalg.norm(vec2, axis=-1, keepdims=True) + 1e-10) + inner_products = np.sum(normalized_vec1 * normalized_vec2, axis=-1) # [n_squares, 4] + squares_degree = np.arccos(inner_products) * 180 / np.pi # [n_squares, 4] + + # get square score + overlap_scores = [] + degree_scores = [] + length_scores = [] + + for connects, segments, square, degree in zip(connect_array, segments_array, squares, squares_degree, strict=False): + ''' + 0 -- 1 + | | + 3 -- 2 + + # segments: [4, 2] + # connects: [4] + ''' + + ###################################### OVERLAP SCORES + cover = 0 + perimeter = 0 + # check 0 > 1 > 2 > 3 + square_length = [] + + for start_idx in range(4): + end_idx = (start_idx + 1) % 4 + + connect_idx = connects[start_idx] # segment idx of segment01 + start_segments = segments[start_idx] + end_segments = segments[end_idx] + + start_point = square[start_idx] + end_point = square[end_idx] + + # check whether outside or inside + start_position, start_min, start_cover_param, start_peri_param = check_outside_inside(start_segments, + connect_idx) + end_position, end_min, end_cover_param, end_peri_param = check_outside_inside(end_segments, connect_idx) + + cover += dist_segments[connect_idx] + start_cover_param * start_min + end_cover_param * end_min + perimeter += dist_segments[connect_idx] + start_peri_param * start_min + end_peri_param * end_min + + square_length.append( + dist_segments[connect_idx] + start_peri_param * start_min + end_peri_param * end_min) + + overlap_scores.append(cover / perimeter) + ###################################### + ###################################### DEGREE SCORES + ''' + deg0 vs deg2 + deg1 vs deg3 + ''' + deg0, deg1, deg2, deg3 = degree + deg_ratio1 = deg0 / deg2 + if deg_ratio1 > 1.0: + deg_ratio1 = 1 / deg_ratio1 + deg_ratio2 = deg1 / deg3 + if deg_ratio2 > 1.0: + deg_ratio2 = 1 / deg_ratio2 + degree_scores.append((deg_ratio1 + deg_ratio2) / 2) + ###################################### + ###################################### LENGTH SCORES + ''' + len0 vs len2 + len1 vs len3 + ''' + len0, len1, len2, len3 = square_length + len_ratio1 = len0 / len2 if len2 > len0 else len2 / len0 + len_ratio2 = len1 / len3 if len3 > len1 else len3 / len1 + length_scores.append((len_ratio1 + len_ratio2) / 2) + + ###################################### + + overlap_scores = np.array(overlap_scores) + overlap_scores /= np.max(overlap_scores) + + degree_scores = np.array(degree_scores) + # degree_scores /= np.max(degree_scores) + + length_scores = np.array(length_scores) + + ###################################### AREA SCORES + area_scores = np.reshape(squares, [-1, 4, 2]) + area_x = area_scores[:, :, 0] + area_y = area_scores[:, :, 1] + correction = area_x[:, -1] * area_y[:, 0] - area_y[:, -1] * area_x[:, 0] + area_scores = np.sum(area_x[:, :-1] * area_y[:, 1:], axis=-1) - np.sum(area_y[:, :-1] * area_x[:, 1:], axis=-1) + area_scores = 0.5 * np.abs(area_scores + correction) + area_scores /= (map_size * map_size) # np.max(area_scores) + ###################################### + + ###################################### CENTER SCORES + centers = np.array([[256 // 2, 256 // 2]], dtype='float32') # [1, 2] + # squares: [n, 4, 2] + square_centers = np.mean(squares, axis=1) # [n, 2] + center2center = np.sqrt(np.sum((centers - square_centers) ** 2)) + center_scores = center2center / (map_size / np.sqrt(2.0)) + + ''' + score_w = [overlap, degree, area, center, length] + ''' + score_w = [0.0, 1.0, 10.0, 0.5, 1.0] + score_array = params['w_overlap'] * overlap_scores \ + + params['w_degree'] * degree_scores \ + + params['w_area'] * area_scores \ + - params['w_center'] * center_scores \ + + params['w_length'] * length_scores + + best_square = [] + + sorted_idx = np.argsort(score_array)[::-1] + score_array = score_array[sorted_idx] + squares = squares[sorted_idx] + + except Exception: + pass + + '''return list + merged_lines, squares, scores + ''' + + try: + new_segments[:, 0] = new_segments[:, 0] * 2 / input_shape[1] * original_shape[1] + new_segments[:, 1] = new_segments[:, 1] * 2 / input_shape[0] * original_shape[0] + new_segments[:, 2] = new_segments[:, 2] * 2 / input_shape[1] * original_shape[1] + new_segments[:, 3] = new_segments[:, 3] * 2 / input_shape[0] * original_shape[0] + except Exception: + new_segments = [] + + try: + squares[:, :, 0] = squares[:, :, 0] * 2 / input_shape[1] * original_shape[1] + squares[:, :, 1] = squares[:, :, 1] * 2 / input_shape[0] * original_shape[0] + except Exception: + squares = [] + score_array = [] + + try: + inter_points = np.array(inter_points) + inter_points[:, 0] = inter_points[:, 0] * 2 / input_shape[1] * original_shape[1] + inter_points[:, 1] = inter_points[:, 1] * 2 / input_shape[0] * original_shape[0] + except Exception: + inter_points = [] + + return new_segments, squares, score_array, inter_points diff --git a/invokeai/backend/image_util/normal_bae/LICENSE b/invokeai/backend/image_util/normal_bae/LICENSE new file mode 100644 index 00000000000..16a9d56a3d4 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Caroline Chan + +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. \ No newline at end of file diff --git a/invokeai/backend/image_util/normal_bae/__init__.py b/invokeai/backend/image_util/normal_bae/__init__.py new file mode 100644 index 00000000000..5ad221ecd4a --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/__init__.py @@ -0,0 +1,94 @@ +# Adapted from https://github.com/huggingface/controlnet_aux + +import pathlib +import types + +import cv2 +import huggingface_hub +import numpy as np +import torch +import torchvision.transforms as transforms +from einops import rearrange +from PIL import Image + +from invokeai.backend.image_util.normal_bae.nets.NNET import NNET +from invokeai.backend.image_util.util import np_to_pil, pil_to_np, resize_to_multiple +from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device + + +class NormalMapDetector: + """Simple wrapper around the Normal BAE model for normal map generation.""" + + hf_repo_id = "lllyasviel/Annotators" + hf_filename = "scannet.pt" + + @classmethod + def get_model_url(cls) -> str: + """Get the URL to download the model from the Hugging Face Hub.""" + return huggingface_hub.hf_hub_url(cls.hf_repo_id, cls.hf_filename) + + @classmethod + def load_model(cls, model_path: pathlib.Path) -> NNET: + """Load the model from a file.""" + + args = types.SimpleNamespace() + args.mode = "client" + args.architecture = "BN" + args.pretrained = "scannet" + args.sampling_ratio = 0.4 + args.importance_ratio = 0.7 + + model = NNET(args) + + ckpt = torch.load(model_path, map_location="cpu")["model"] + load_dict = {} + for k, v in ckpt.items(): + if k.startswith("module."): + k_ = k.replace("module.", "") + load_dict[k_] = v + else: + load_dict[k] = v + + model.load_state_dict(load_dict) + model.eval() + + return model + + def __init__(self, model: NNET) -> None: + self.model = model + self.norm = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) + + def to(self, device: torch.device): + self.model.to(device) + return self + + def run(self, image: Image.Image): + """Processes an image and returns the detected normal map.""" + + device = get_effective_device(self.model) + np_image = pil_to_np(image) + + height, width, _channels = np_image.shape + + # The model requires the image to be a multiple of 8 + np_image = resize_to_multiple(np_image, 8) + + image_normal = np_image + + with torch.no_grad(): + image_normal = torch.from_numpy(image_normal).float().to(device) + image_normal = image_normal / 255.0 + image_normal = rearrange(image_normal, "h w c -> 1 c h w") + image_normal = self.norm(image_normal) + + normal = self.model(image_normal) + normal = normal[0][-1][:, :3] + normal = ((normal + 1) * 0.5).clip(0, 1) + + normal = rearrange(normal[0], "c h w -> h w c").cpu().numpy() + normal_image = (normal * 255.0).clip(0, 255).astype(np.uint8) + + # Back to the original size + output_image = cv2.resize(normal_image, (width, height), interpolation=cv2.INTER_LINEAR) + + return np_to_pil(output_image) diff --git a/invokeai/backend/image_util/normal_bae/nets/NNET.py b/invokeai/backend/image_util/normal_bae/nets/NNET.py new file mode 100644 index 00000000000..3ddbc50c3ac --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/NNET.py @@ -0,0 +1,22 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +from .submodules.encoder import Encoder +from .submodules.decoder import Decoder + + +class NNET(nn.Module): + def __init__(self, args): + super(NNET, self).__init__() + self.encoder = Encoder() + self.decoder = Decoder(args) + + def get_1x_lr_params(self): # lr/10 learning rate + return self.encoder.parameters() + + def get_10x_lr_params(self): # lr learning rate + return self.decoder.parameters() + + def forward(self, img, **kwargs): + return self.decoder(self.encoder(img), **kwargs) \ No newline at end of file diff --git a/invokeai/backend/image_util/normal_bae/nets/__init__.py b/invokeai/backend/image_util/normal_bae/nets/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/image_util/normal_bae/nets/baseline.py b/invokeai/backend/image_util/normal_bae/nets/baseline.py new file mode 100644 index 00000000000..602d0fbdac1 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/baseline.py @@ -0,0 +1,85 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +from .submodules.submodules import UpSampleBN, norm_normalize + + +# This is the baseline encoder-decoder we used in the ablation study +class NNET(nn.Module): + def __init__(self, args=None): + super(NNET, self).__init__() + self.encoder = Encoder() + self.decoder = Decoder(num_classes=4) + + def forward(self, x, **kwargs): + out = self.decoder(self.encoder(x), **kwargs) + + # Bilinearly upsample the output to match the input resolution + up_out = F.interpolate(out, size=[x.size(2), x.size(3)], mode='bilinear', align_corners=False) + + # L2-normalize the first three channels / ensure positive value for concentration parameters (kappa) + up_out = norm_normalize(up_out) + return up_out + + def get_1x_lr_params(self): # lr/10 learning rate + return self.encoder.parameters() + + def get_10x_lr_params(self): # lr learning rate + modules = [self.decoder] + for m in modules: + yield from m.parameters() + + +# Encoder +class Encoder(nn.Module): + def __init__(self): + super(Encoder, self).__init__() + + basemodel_name = 'tf_efficientnet_b5_ap' + basemodel = torch.hub.load('rwightman/gen-efficientnet-pytorch', basemodel_name, pretrained=True) + + # Remove last layer + basemodel.global_pool = nn.Identity() + basemodel.classifier = nn.Identity() + + self.original_model = basemodel + + def forward(self, x): + features = [x] + for k, v in self.original_model._modules.items(): + if (k == 'blocks'): + for ki, vi in v._modules.items(): + features.append(vi(features[-1])) + else: + features.append(v(features[-1])) + return features + + +# Decoder (no pixel-wise MLP, no uncertainty-guided sampling) +class Decoder(nn.Module): + def __init__(self, num_classes=4): + super(Decoder, self).__init__() + self.conv2 = nn.Conv2d(2048, 2048, kernel_size=1, stride=1, padding=0) + self.up1 = UpSampleBN(skip_input=2048 + 176, output_features=1024) + self.up2 = UpSampleBN(skip_input=1024 + 64, output_features=512) + self.up3 = UpSampleBN(skip_input=512 + 40, output_features=256) + self.up4 = UpSampleBN(skip_input=256 + 24, output_features=128) + self.conv3 = nn.Conv2d(128, num_classes, kernel_size=3, stride=1, padding=1) + + def forward(self, features): + x_block0, x_block1, x_block2, x_block3, x_block4 = features[4], features[5], features[6], features[8], features[11] + x_d0 = self.conv2(x_block4) + x_d1 = self.up1(x_d0, x_block3) + x_d2 = self.up2(x_d1, x_block2) + x_d3 = self.up3(x_d2, x_block1) + x_d4 = self.up4(x_d3, x_block0) + out = self.conv3(x_d4) + return out + + +if __name__ == '__main__': + model = Baseline() + x = torch.rand(2, 3, 480, 640) + out = model(x) + print(out.shape) diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/__init__.py b/invokeai/backend/image_util/normal_bae/nets/submodules/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/decoder.py b/invokeai/backend/image_util/normal_bae/nets/submodules/decoder.py new file mode 100644 index 00000000000..993203d1792 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/decoder.py @@ -0,0 +1,202 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from .submodules import UpSampleBN, UpSampleGN, norm_normalize, sample_points + + +class Decoder(nn.Module): + def __init__(self, args): + super(Decoder, self).__init__() + + # hyper-parameter for sampling + self.sampling_ratio = args.sampling_ratio + self.importance_ratio = args.importance_ratio + + # feature-map + self.conv2 = nn.Conv2d(2048, 2048, kernel_size=1, stride=1, padding=0) + if args.architecture == 'BN': + self.up1 = UpSampleBN(skip_input=2048 + 176, output_features=1024) + self.up2 = UpSampleBN(skip_input=1024 + 64, output_features=512) + self.up3 = UpSampleBN(skip_input=512 + 40, output_features=256) + self.up4 = UpSampleBN(skip_input=256 + 24, output_features=128) + + elif args.architecture == 'GN': + self.up1 = UpSampleGN(skip_input=2048 + 176, output_features=1024) + self.up2 = UpSampleGN(skip_input=1024 + 64, output_features=512) + self.up3 = UpSampleGN(skip_input=512 + 40, output_features=256) + self.up4 = UpSampleGN(skip_input=256 + 24, output_features=128) + + else: + raise Exception('invalid architecture') + + # produces 1/8 res output + self.out_conv_res8 = nn.Conv2d(512, 4, kernel_size=3, stride=1, padding=1) + + # produces 1/4 res output + self.out_conv_res4 = nn.Sequential( + nn.Conv1d(512 + 4, 128, kernel_size=1), nn.ReLU(), + nn.Conv1d(128, 128, kernel_size=1), nn.ReLU(), + nn.Conv1d(128, 128, kernel_size=1), nn.ReLU(), + nn.Conv1d(128, 4, kernel_size=1), + ) + + # produces 1/2 res output + self.out_conv_res2 = nn.Sequential( + nn.Conv1d(256 + 4, 128, kernel_size=1), nn.ReLU(), + nn.Conv1d(128, 128, kernel_size=1), nn.ReLU(), + nn.Conv1d(128, 128, kernel_size=1), nn.ReLU(), + nn.Conv1d(128, 4, kernel_size=1), + ) + + # produces 1/1 res output + self.out_conv_res1 = nn.Sequential( + nn.Conv1d(128 + 4, 128, kernel_size=1), nn.ReLU(), + nn.Conv1d(128, 128, kernel_size=1), nn.ReLU(), + nn.Conv1d(128, 128, kernel_size=1), nn.ReLU(), + nn.Conv1d(128, 4, kernel_size=1), + ) + + def forward(self, features, gt_norm_mask=None, mode='test'): + x_block0, x_block1, x_block2, x_block3, x_block4 = features[4], features[5], features[6], features[8], features[11] + + # generate feature-map + + x_d0 = self.conv2(x_block4) # x_d0 : [2, 2048, 15, 20] 1/32 res + x_d1 = self.up1(x_d0, x_block3) # x_d1 : [2, 1024, 30, 40] 1/16 res + x_d2 = self.up2(x_d1, x_block2) # x_d2 : [2, 512, 60, 80] 1/8 res + x_d3 = self.up3(x_d2, x_block1) # x_d3: [2, 256, 120, 160] 1/4 res + x_d4 = self.up4(x_d3, x_block0) # x_d4: [2, 128, 240, 320] 1/2 res + + # 1/8 res output + out_res8 = self.out_conv_res8(x_d2) # out_res8: [2, 4, 60, 80] 1/8 res output + out_res8 = norm_normalize(out_res8) # out_res8: [2, 4, 60, 80] 1/8 res output + + ################################################################################################################ + # out_res4 + ################################################################################################################ + + if mode == 'train': + # upsampling ... out_res8: [2, 4, 60, 80] -> out_res8_res4: [2, 4, 120, 160] + out_res8_res4 = F.interpolate(out_res8, scale_factor=2, mode='bilinear', align_corners=True) + B, _, H, W = out_res8_res4.shape + + # samples: [B, 1, N, 2] + point_coords_res4, rows_int, cols_int = sample_points(out_res8_res4.detach(), gt_norm_mask, + sampling_ratio=self.sampling_ratio, + beta=self.importance_ratio) + + # output (needed for evaluation / visualization) + out_res4 = out_res8_res4 + + # grid_sample feature-map + feat_res4 = F.grid_sample(x_d2, point_coords_res4, mode='bilinear', align_corners=True) # (B, 512, 1, N) + init_pred = F.grid_sample(out_res8, point_coords_res4, mode='bilinear', align_corners=True) # (B, 4, 1, N) + feat_res4 = torch.cat([feat_res4, init_pred], dim=1) # (B, 512+4, 1, N) + + # prediction (needed to compute loss) + samples_pred_res4 = self.out_conv_res4(feat_res4[:, :, 0, :]) # (B, 4, N) + samples_pred_res4 = norm_normalize(samples_pred_res4) # (B, 4, N) - normalized + + for i in range(B): + out_res4[i, :, rows_int[i, :], cols_int[i, :]] = samples_pred_res4[i, :, :] + + else: + # grid_sample feature-map + feat_map = F.interpolate(x_d2, scale_factor=2, mode='bilinear', align_corners=True) + init_pred = F.interpolate(out_res8, scale_factor=2, mode='bilinear', align_corners=True) + feat_map = torch.cat([feat_map, init_pred], dim=1) # (B, 512+4, H, W) + B, _, H, W = feat_map.shape + + # try all pixels + out_res4 = self.out_conv_res4(feat_map.view(B, 512 + 4, -1)) # (B, 4, N) + out_res4 = norm_normalize(out_res4) # (B, 4, N) - normalized + out_res4 = out_res4.view(B, 4, H, W) + samples_pred_res4 = point_coords_res4 = None + + ################################################################################################################ + # out_res2 + ################################################################################################################ + + if mode == 'train': + + # upsampling ... out_res4: [2, 4, 120, 160] -> out_res4_res2: [2, 4, 240, 320] + out_res4_res2 = F.interpolate(out_res4, scale_factor=2, mode='bilinear', align_corners=True) + B, _, H, W = out_res4_res2.shape + + # samples: [B, 1, N, 2] + point_coords_res2, rows_int, cols_int = sample_points(out_res4_res2.detach(), gt_norm_mask, + sampling_ratio=self.sampling_ratio, + beta=self.importance_ratio) + + # output (needed for evaluation / visualization) + out_res2 = out_res4_res2 + + # grid_sample feature-map + feat_res2 = F.grid_sample(x_d3, point_coords_res2, mode='bilinear', align_corners=True) # (B, 256, 1, N) + init_pred = F.grid_sample(out_res4, point_coords_res2, mode='bilinear', align_corners=True) # (B, 4, 1, N) + feat_res2 = torch.cat([feat_res2, init_pred], dim=1) # (B, 256+4, 1, N) + + # prediction (needed to compute loss) + samples_pred_res2 = self.out_conv_res2(feat_res2[:, :, 0, :]) # (B, 4, N) + samples_pred_res2 = norm_normalize(samples_pred_res2) # (B, 4, N) - normalized + + for i in range(B): + out_res2[i, :, rows_int[i, :], cols_int[i, :]] = samples_pred_res2[i, :, :] + + else: + # grid_sample feature-map + feat_map = F.interpolate(x_d3, scale_factor=2, mode='bilinear', align_corners=True) + init_pred = F.interpolate(out_res4, scale_factor=2, mode='bilinear', align_corners=True) + feat_map = torch.cat([feat_map, init_pred], dim=1) # (B, 512+4, H, W) + B, _, H, W = feat_map.shape + + out_res2 = self.out_conv_res2(feat_map.view(B, 256 + 4, -1)) # (B, 4, N) + out_res2 = norm_normalize(out_res2) # (B, 4, N) - normalized + out_res2 = out_res2.view(B, 4, H, W) + samples_pred_res2 = point_coords_res2 = None + + ################################################################################################################ + # out_res1 + ################################################################################################################ + + if mode == 'train': + # upsampling ... out_res4: [2, 4, 120, 160] -> out_res4_res2: [2, 4, 240, 320] + out_res2_res1 = F.interpolate(out_res2, scale_factor=2, mode='bilinear', align_corners=True) + B, _, H, W = out_res2_res1.shape + + # samples: [B, 1, N, 2] + point_coords_res1, rows_int, cols_int = sample_points(out_res2_res1.detach(), gt_norm_mask, + sampling_ratio=self.sampling_ratio, + beta=self.importance_ratio) + + # output (needed for evaluation / visualization) + out_res1 = out_res2_res1 + + # grid_sample feature-map + feat_res1 = F.grid_sample(x_d4, point_coords_res1, mode='bilinear', align_corners=True) # (B, 128, 1, N) + init_pred = F.grid_sample(out_res2, point_coords_res1, mode='bilinear', align_corners=True) # (B, 4, 1, N) + feat_res1 = torch.cat([feat_res1, init_pred], dim=1) # (B, 128+4, 1, N) + + # prediction (needed to compute loss) + samples_pred_res1 = self.out_conv_res1(feat_res1[:, :, 0, :]) # (B, 4, N) + samples_pred_res1 = norm_normalize(samples_pred_res1) # (B, 4, N) - normalized + + for i in range(B): + out_res1[i, :, rows_int[i, :], cols_int[i, :]] = samples_pred_res1[i, :, :] + + else: + # grid_sample feature-map + feat_map = F.interpolate(x_d4, scale_factor=2, mode='bilinear', align_corners=True) + init_pred = F.interpolate(out_res2, scale_factor=2, mode='bilinear', align_corners=True) + feat_map = torch.cat([feat_map, init_pred], dim=1) # (B, 512+4, H, W) + B, _, H, W = feat_map.shape + + out_res1 = self.out_conv_res1(feat_map.view(B, 128 + 4, -1)) # (B, 4, N) + out_res1 = norm_normalize(out_res1) # (B, 4, N) - normalized + out_res1 = out_res1.view(B, 4, H, W) + samples_pred_res1 = point_coords_res1 = None + + return [out_res8, out_res4, out_res2, out_res1], \ + [out_res8, samples_pred_res4, samples_pred_res2, samples_pred_res1], \ + [None, point_coords_res4, point_coords_res2, point_coords_res1] + diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/.gitignore b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/.gitignore new file mode 100644 index 00000000000..f04e5fff910 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/.gitignore @@ -0,0 +1,109 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# pytorch stuff +*.pth +*.onnx +*.pb + +trained_models/ +.fuse_hidden* diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/BENCHMARK.md b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/BENCHMARK.md new file mode 100644 index 00000000000..6ead7171ce5 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/BENCHMARK.md @@ -0,0 +1,555 @@ +# Model Performance Benchmarks + +All benchmarks run as per: + +``` +python onnx_export.py --model mobilenetv3_100 ./mobilenetv3_100.onnx +python onnx_optimize.py ./mobilenetv3_100.onnx --output mobilenetv3_100-opt.onnx +python onnx_to_caffe.py ./mobilenetv3_100.onnx --c2-prefix mobilenetv3 +python onnx_to_caffe.py ./mobilenetv3_100-opt.onnx --c2-prefix mobilenetv3-opt +python caffe2_benchmark.py --c2-init ./mobilenetv3.init.pb --c2-predict ./mobilenetv3.predict.pb +python caffe2_benchmark.py --c2-init ./mobilenetv3-opt.init.pb --c2-predict ./mobilenetv3-opt.predict.pb +``` + +## EfficientNet-B0 + +### Unoptimized +``` +Main run finished. Milliseconds per iter: 49.2862. Iters per second: 20.2897 +Time per operator type: + 29.7378 ms. 60.5145%. Conv + 12.1785 ms. 24.7824%. Sigmoid + 3.62811 ms. 7.38297%. SpatialBN + 2.98444 ms. 6.07314%. Mul + 0.326902 ms. 0.665225%. AveragePool + 0.197317 ms. 0.401528%. FC + 0.0852877 ms. 0.173555%. Add + 0.0032607 ms. 0.00663532%. Squeeze + 49.1416 ms in Total +FLOP per operator type: + 0.76907 GFLOP. 95.2696%. Conv + 0.0269508 GFLOP. 3.33857%. SpatialBN + 0.00846444 GFLOP. 1.04855%. Mul + 0.002561 GFLOP. 0.317248%. FC + 0.000210112 GFLOP. 0.0260279%. Add + 0.807256 GFLOP in Total +Feature Memory Read per operator type: + 58.5253 MB. 43.0891%. Mul + 43.2015 MB. 31.807%. Conv + 27.2869 MB. 20.0899%. SpatialBN + 5.12912 MB. 3.77631%. FC + 1.6809 MB. 1.23756%. Add + 135.824 MB in Total +Feature Memory Written per operator type: + 33.8578 MB. 38.1965%. Mul + 26.9881 MB. 30.4465%. Conv + 26.9508 MB. 30.4044%. SpatialBN + 0.840448 MB. 0.948147%. Add + 0.004 MB. 0.00451258%. FC + 88.6412 MB in Total +Parameter Memory per operator type: + 15.8248 MB. 74.9391%. Conv + 5.124 MB. 24.265%. FC + 0.168064 MB. 0.795877%. SpatialBN + 0 MB. 0%. Add + 0 MB. 0%. Mul + 21.1168 MB in Total +``` +### Optimized +``` +Main run finished. Milliseconds per iter: 46.0838. Iters per second: 21.6996 +Time per operator type: + 29.776 ms. 65.002%. Conv + 12.2803 ms. 26.8084%. Sigmoid + 3.15073 ms. 6.87815%. Mul + 0.328651 ms. 0.717456%. AveragePool + 0.186237 ms. 0.406563%. FC + 0.0832429 ms. 0.181722%. Add + 0.0026184 ms. 0.00571606%. Squeeze + 45.8078 ms in Total +FLOP per operator type: + 0.76907 GFLOP. 98.5601%. Conv + 0.00846444 GFLOP. 1.08476%. Mul + 0.002561 GFLOP. 0.328205%. FC + 0.000210112 GFLOP. 0.0269269%. Add + 0.780305 GFLOP in Total +Feature Memory Read per operator type: + 58.5253 MB. 53.8803%. Mul + 43.2855 MB. 39.8501%. Conv + 5.12912 MB. 4.72204%. FC + 1.6809 MB. 1.54749%. Add + 108.621 MB in Total +Feature Memory Written per operator type: + 33.8578 MB. 54.8834%. Mul + 26.9881 MB. 43.7477%. Conv + 0.840448 MB. 1.36237%. Add + 0.004 MB. 0.00648399%. FC + 61.6904 MB in Total +Parameter Memory per operator type: + 15.8248 MB. 75.5403%. Conv + 5.124 MB. 24.4597%. FC + 0 MB. 0%. Add + 0 MB. 0%. Mul + 20.9488 MB in Total +``` + +## EfficientNet-B1 +### Optimized +``` +Main run finished. Milliseconds per iter: 71.8102. Iters per second: 13.9256 +Time per operator type: + 45.7915 ms. 66.3206%. Conv + 17.8718 ms. 25.8841%. Sigmoid + 4.44132 ms. 6.43244%. Mul + 0.51001 ms. 0.738658%. AveragePool + 0.233283 ms. 0.337868%. Add + 0.194986 ms. 0.282402%. FC + 0.00268255 ms. 0.00388519%. Squeeze + 69.0456 ms in Total +FLOP per operator type: + 1.37105 GFLOP. 98.7673%. Conv + 0.0138759 GFLOP. 0.99959%. Mul + 0.002561 GFLOP. 0.184489%. FC + 0.000674432 GFLOP. 0.0485847%. Add + 1.38816 GFLOP in Total +Feature Memory Read per operator type: + 94.624 MB. 54.0789%. Mul + 69.8255 MB. 39.9062%. Conv + 5.39546 MB. 3.08357%. Add + 5.12912 MB. 2.93136%. FC + 174.974 MB in Total +Feature Memory Written per operator type: + 55.5035 MB. 54.555%. Mul + 43.5333 MB. 42.7894%. Conv + 2.69773 MB. 2.65163%. Add + 0.004 MB. 0.00393165%. FC + 101.739 MB in Total +Parameter Memory per operator type: + 25.7479 MB. 83.4024%. Conv + 5.124 MB. 16.5976%. FC + 0 MB. 0%. Add + 0 MB. 0%. Mul + 30.8719 MB in Total +``` + +## EfficientNet-B2 +### Optimized +``` +Main run finished. Milliseconds per iter: 92.28. Iters per second: 10.8366 +Time per operator type: + 61.4627 ms. 67.5845%. Conv + 22.7458 ms. 25.0113%. Sigmoid + 5.59931 ms. 6.15701%. Mul + 0.642567 ms. 0.706568%. AveragePool + 0.272795 ms. 0.299965%. Add + 0.216178 ms. 0.237709%. FC + 0.00268895 ms. 0.00295677%. Squeeze + 90.942 ms in Total +FLOP per operator type: + 1.98431 GFLOP. 98.9343%. Conv + 0.0177039 GFLOP. 0.882686%. Mul + 0.002817 GFLOP. 0.140451%. FC + 0.000853984 GFLOP. 0.0425782%. Add + 2.00568 GFLOP in Total +Feature Memory Read per operator type: + 120.609 MB. 54.9637%. Mul + 86.3512 MB. 39.3519%. Conv + 6.83187 MB. 3.11341%. Add + 5.64163 MB. 2.571%. FC + 219.433 MB in Total +Feature Memory Written per operator type: + 70.8155 MB. 54.6573%. Mul + 55.3273 MB. 42.7031%. Conv + 3.41594 MB. 2.63651%. Add + 0.004 MB. 0.00308731%. FC + 129.563 MB in Total +Parameter Memory per operator type: + 30.4721 MB. 84.3913%. Conv + 5.636 MB. 15.6087%. FC + 0 MB. 0%. Add + 0 MB. 0%. Mul + 36.1081 MB in Total +``` + +## MixNet-M +### Optimized +``` +Main run finished. Milliseconds per iter: 63.1122. Iters per second: 15.8448 +Time per operator type: + 48.1139 ms. 75.2052%. Conv + 7.1341 ms. 11.1511%. Sigmoid + 2.63706 ms. 4.12189%. SpatialBN + 1.73186 ms. 2.70701%. Mul + 1.38707 ms. 2.16809%. Split + 1.29322 ms. 2.02139%. Concat + 1.00093 ms. 1.56452%. Relu + 0.235309 ms. 0.367803%. Add + 0.221579 ms. 0.346343%. FC + 0.219315 ms. 0.342803%. AveragePool + 0.00250145 ms. 0.00390993%. Squeeze + 63.9768 ms in Total +FLOP per operator type: + 0.675273 GFLOP. 95.5827%. Conv + 0.0221072 GFLOP. 3.12921%. SpatialBN + 0.00538445 GFLOP. 0.762152%. Mul + 0.003073 GFLOP. 0.434973%. FC + 0.000642488 GFLOP. 0.0909421%. Add + 0 GFLOP. 0%. Concat + 0 GFLOP. 0%. Relu + 0.70648 GFLOP in Total +Feature Memory Read per operator type: + 46.8424 MB. 30.502%. Conv + 36.8626 MB. 24.0036%. Mul + 22.3152 MB. 14.5309%. SpatialBN + 22.1074 MB. 14.3955%. Concat + 14.1496 MB. 9.21372%. Relu + 6.15414 MB. 4.00735%. FC + 5.1399 MB. 3.34692%. Add + 153.571 MB in Total +Feature Memory Written per operator type: + 32.7672 MB. 28.4331%. Conv + 22.1072 MB. 19.1831%. Concat + 22.1072 MB. 19.1831%. SpatialBN + 21.5378 MB. 18.689%. Mul + 14.1496 MB. 12.2781%. Relu + 2.56995 MB. 2.23003%. Add + 0.004 MB. 0.00347092%. FC + 115.243 MB in Total +Parameter Memory per operator type: + 13.7059 MB. 68.674%. Conv + 6.148 MB. 30.8049%. FC + 0.104 MB. 0.521097%. SpatialBN + 0 MB. 0%. Add + 0 MB. 0%. Concat + 0 MB. 0%. Mul + 0 MB. 0%. Relu + 19.9579 MB in Total +``` + +## TF MobileNet-V3 Large 1.0 + +### Optimized +``` +Main run finished. Milliseconds per iter: 22.0495. Iters per second: 45.3525 +Time per operator type: + 17.437 ms. 80.0087%. Conv + 1.27662 ms. 5.8577%. Add + 1.12759 ms. 5.17387%. Div + 0.701155 ms. 3.21721%. Mul + 0.562654 ms. 2.58171%. Relu + 0.431144 ms. 1.97828%. Clip + 0.156902 ms. 0.719936%. FC + 0.0996858 ms. 0.457402%. AveragePool + 0.00112455 ms. 0.00515993%. Flatten + 21.7939 ms in Total +FLOP per operator type: + 0.43062 GFLOP. 98.1484%. Conv + 0.002561 GFLOP. 0.583713%. FC + 0.00210867 GFLOP. 0.480616%. Mul + 0.00193868 GFLOP. 0.441871%. Add + 0.00151532 GFLOP. 0.345377%. Div + 0 GFLOP. 0%. Relu + 0.438743 GFLOP in Total +Feature Memory Read per operator type: + 34.7967 MB. 43.9391%. Conv + 14.496 MB. 18.3046%. Mul + 9.44828 MB. 11.9307%. Add + 9.26157 MB. 11.6949%. Relu + 6.0614 MB. 7.65395%. Div + 5.12912 MB. 6.47673%. FC + 79.193 MB in Total +Feature Memory Written per operator type: + 17.6247 MB. 35.8656%. Conv + 9.26157 MB. 18.847%. Relu + 8.43469 MB. 17.1643%. Mul + 7.75472 MB. 15.7806%. Add + 6.06128 MB. 12.3345%. Div + 0.004 MB. 0.00813985%. FC + 49.1409 MB in Total +Parameter Memory per operator type: + 16.6851 MB. 76.5052%. Conv + 5.124 MB. 23.4948%. FC + 0 MB. 0%. Add + 0 MB. 0%. Div + 0 MB. 0%. Mul + 0 MB. 0%. Relu + 21.8091 MB in Total +``` + +## MobileNet-V3 (RW) + +### Unoptimized +``` +Main run finished. Milliseconds per iter: 24.8316. Iters per second: 40.2712 +Time per operator type: + 15.9266 ms. 69.2624%. Conv + 2.36551 ms. 10.2873%. SpatialBN + 1.39102 ms. 6.04936%. Add + 1.30327 ms. 5.66773%. Div + 0.737014 ms. 3.20517%. Mul + 0.639697 ms. 2.78195%. Relu + 0.375681 ms. 1.63378%. Clip + 0.153126 ms. 0.665921%. FC + 0.0993787 ms. 0.432184%. AveragePool + 0.0032632 ms. 0.0141912%. Squeeze + 22.9946 ms in Total +FLOP per operator type: + 0.430616 GFLOP. 94.4041%. Conv + 0.0175992 GFLOP. 3.85829%. SpatialBN + 0.002561 GFLOP. 0.561449%. FC + 0.00210961 GFLOP. 0.46249%. Mul + 0.00173891 GFLOP. 0.381223%. Add + 0.00151626 GFLOP. 0.33241%. Div + 0 GFLOP. 0%. Relu + 0.456141 GFLOP in Total +Feature Memory Read per operator type: + 34.7354 MB. 36.4363%. Conv + 17.7944 MB. 18.6658%. SpatialBN + 14.5035 MB. 15.2137%. Mul + 9.25778 MB. 9.71113%. Relu + 7.84641 MB. 8.23064%. Add + 6.06516 MB. 6.36216%. Div + 5.12912 MB. 5.38029%. FC + 95.3317 MB in Total +Feature Memory Written per operator type: + 17.6246 MB. 26.7264%. Conv + 17.5992 MB. 26.6878%. SpatialBN + 9.25778 MB. 14.0387%. Relu + 8.43843 MB. 12.7962%. Mul + 6.95565 MB. 10.5477%. Add + 6.06502 MB. 9.19713%. Div + 0.004 MB. 0.00606568%. FC + 65.9447 MB in Total +Parameter Memory per operator type: + 16.6778 MB. 76.1564%. Conv + 5.124 MB. 23.3979%. FC + 0.0976 MB. 0.445674%. SpatialBN + 0 MB. 0%. Add + 0 MB. 0%. Div + 0 MB. 0%. Mul + 0 MB. 0%. Relu + 21.8994 MB in Total + +``` +### Optimized + +``` +Main run finished. Milliseconds per iter: 22.0981. Iters per second: 45.2527 +Time per operator type: + 17.146 ms. 78.8965%. Conv + 1.38453 ms. 6.37084%. Add + 1.30991 ms. 6.02749%. Div + 0.685417 ms. 3.15391%. Mul + 0.532589 ms. 2.45068%. Relu + 0.418263 ms. 1.92461%. Clip + 0.15128 ms. 0.696106%. FC + 0.102065 ms. 0.469648%. AveragePool + 0.0022143 ms. 0.010189%. Squeeze + 21.7323 ms in Total +FLOP per operator type: + 0.430616 GFLOP. 98.1927%. Conv + 0.002561 GFLOP. 0.583981%. FC + 0.00210961 GFLOP. 0.481051%. Mul + 0.00173891 GFLOP. 0.396522%. Add + 0.00151626 GFLOP. 0.34575%. Div + 0 GFLOP. 0%. Relu + 0.438542 GFLOP in Total +Feature Memory Read per operator type: + 34.7842 MB. 44.833%. Conv + 14.5035 MB. 18.6934%. Mul + 9.25778 MB. 11.9323%. Relu + 7.84641 MB. 10.1132%. Add + 6.06516 MB. 7.81733%. Div + 5.12912 MB. 6.61087%. FC + 77.5861 MB in Total +Feature Memory Written per operator type: + 17.6246 MB. 36.4556%. Conv + 9.25778 MB. 19.1492%. Relu + 8.43843 MB. 17.4544%. Mul + 6.95565 MB. 14.3874%. Add + 6.06502 MB. 12.5452%. Div + 0.004 MB. 0.00827378%. FC + 48.3455 MB in Total +Parameter Memory per operator type: + 16.6778 MB. 76.4973%. Conv + 5.124 MB. 23.5027%. FC + 0 MB. 0%. Add + 0 MB. 0%. Div + 0 MB. 0%. Mul + 0 MB. 0%. Relu + 21.8018 MB in Total + +``` + +## MnasNet-A1 + +### Unoptimized +``` +Main run finished. Milliseconds per iter: 30.0892. Iters per second: 33.2345 +Time per operator type: + 24.4656 ms. 79.0905%. Conv + 4.14958 ms. 13.4144%. SpatialBN + 1.60598 ms. 5.19169%. Relu + 0.295219 ms. 0.95436%. Mul + 0.187609 ms. 0.606486%. FC + 0.120556 ms. 0.389724%. AveragePool + 0.09036 ms. 0.292109%. Add + 0.015727 ms. 0.050841%. Sigmoid + 0.00306205 ms. 0.00989875%. Squeeze + 30.9337 ms in Total +FLOP per operator type: + 0.620598 GFLOP. 95.6434%. Conv + 0.0248873 GFLOP. 3.8355%. SpatialBN + 0.002561 GFLOP. 0.394688%. FC + 0.000597408 GFLOP. 0.0920695%. Mul + 0.000222656 GFLOP. 0.0343146%. Add + 0 GFLOP. 0%. Relu + 0.648867 GFLOP in Total +Feature Memory Read per operator type: + 35.5457 MB. 38.4109%. Conv + 25.1552 MB. 27.1829%. SpatialBN + 22.5235 MB. 24.339%. Relu + 5.12912 MB. 5.54256%. FC + 2.40586 MB. 2.59978%. Mul + 1.78125 MB. 1.92483%. Add + 92.5406 MB in Total +Feature Memory Written per operator type: + 24.9042 MB. 32.9424%. Conv + 24.8873 MB. 32.92%. SpatialBN + 22.5235 MB. 29.7932%. Relu + 2.38963 MB. 3.16092%. Mul + 0.890624 MB. 1.17809%. Add + 0.004 MB. 0.00529106%. FC + 75.5993 MB in Total +Parameter Memory per operator type: + 10.2732 MB. 66.1459%. Conv + 5.124 MB. 32.9917%. FC + 0.133952 MB. 0.86247%. SpatialBN + 0 MB. 0%. Add + 0 MB. 0%. Mul + 0 MB. 0%. Relu + 15.5312 MB in Total +``` + +### Optimized +``` +Main run finished. Milliseconds per iter: 24.2367. Iters per second: 41.2597 +Time per operator type: + 22.0547 ms. 91.1375%. Conv + 1.49096 ms. 6.16116%. Relu + 0.253417 ms. 1.0472%. Mul + 0.18506 ms. 0.76473%. FC + 0.112942 ms. 0.466717%. AveragePool + 0.086769 ms. 0.358559%. Add + 0.0127889 ms. 0.0528479%. Sigmoid + 0.0027346 ms. 0.0113003%. Squeeze + 24.1994 ms in Total +FLOP per operator type: + 0.620598 GFLOP. 99.4581%. Conv + 0.002561 GFLOP. 0.41043%. FC + 0.000597408 GFLOP. 0.0957417%. Mul + 0.000222656 GFLOP. 0.0356832%. Add + 0 GFLOP. 0%. Relu + 0.623979 GFLOP in Total +Feature Memory Read per operator type: + 35.6127 MB. 52.7968%. Conv + 22.5235 MB. 33.3917%. Relu + 5.12912 MB. 7.60406%. FC + 2.40586 MB. 3.56675%. Mul + 1.78125 MB. 2.64075%. Add + 67.4524 MB in Total +Feature Memory Written per operator type: + 24.9042 MB. 49.1092%. Conv + 22.5235 MB. 44.4145%. Relu + 2.38963 MB. 4.71216%. Mul + 0.890624 MB. 1.75624%. Add + 0.004 MB. 0.00788768%. FC + 50.712 MB in Total +Parameter Memory per operator type: + 10.2732 MB. 66.7213%. Conv + 5.124 MB. 33.2787%. FC + 0 MB. 0%. Add + 0 MB. 0%. Mul + 0 MB. 0%. Relu + 15.3972 MB in Total +``` +## MnasNet-B1 + +### Unoptimized +``` +Main run finished. Milliseconds per iter: 28.3109. Iters per second: 35.322 +Time per operator type: + 29.1121 ms. 83.3081%. Conv + 4.14959 ms. 11.8746%. SpatialBN + 1.35823 ms. 3.88675%. Relu + 0.186188 ms. 0.532802%. FC + 0.116244 ms. 0.332647%. Add + 0.018641 ms. 0.0533437%. AveragePool + 0.0040904 ms. 0.0117052%. Squeeze + 34.9451 ms in Total +FLOP per operator type: + 0.626272 GFLOP. 96.2088%. Conv + 0.0218266 GFLOP. 3.35303%. SpatialBN + 0.002561 GFLOP. 0.393424%. FC + 0.000291648 GFLOP. 0.0448034%. Add + 0 GFLOP. 0%. Relu + 0.650951 GFLOP in Total +Feature Memory Read per operator type: + 34.4354 MB. 41.3788%. Conv + 22.1299 MB. 26.5921%. SpatialBN + 19.1923 MB. 23.0622%. Relu + 5.12912 MB. 6.16333%. FC + 2.33318 MB. 2.80364%. Add + 83.2199 MB in Total +Feature Memory Written per operator type: + 21.8266 MB. 34.0955%. Conv + 21.8266 MB. 34.0955%. SpatialBN + 19.1923 MB. 29.9805%. Relu + 1.16659 MB. 1.82234%. Add + 0.004 MB. 0.00624844%. FC + 64.016 MB in Total +Parameter Memory per operator type: + 12.2576 MB. 69.9104%. Conv + 5.124 MB. 29.2245%. FC + 0.15168 MB. 0.865099%. SpatialBN + 0 MB. 0%. Add + 0 MB. 0%. Relu + 17.5332 MB in Total +``` + +### Optimized +``` +Main run finished. Milliseconds per iter: 26.6364. Iters per second: 37.5426 +Time per operator type: + 24.9888 ms. 94.0962%. Conv + 1.26147 ms. 4.75011%. Relu + 0.176234 ms. 0.663619%. FC + 0.113309 ms. 0.426672%. Add + 0.0138708 ms. 0.0522311%. AveragePool + 0.00295685 ms. 0.0111341%. Squeeze + 26.5566 ms in Total +FLOP per operator type: + 0.626272 GFLOP. 99.5466%. Conv + 0.002561 GFLOP. 0.407074%. FC + 0.000291648 GFLOP. 0.0463578%. Add + 0 GFLOP. 0%. Relu + 0.629124 GFLOP in Total +Feature Memory Read per operator type: + 34.5112 MB. 56.4224%. Conv + 19.1923 MB. 31.3775%. Relu + 5.12912 MB. 8.3856%. FC + 2.33318 MB. 3.81452%. Add + 61.1658 MB in Total +Feature Memory Written per operator type: + 21.8266 MB. 51.7346%. Conv + 19.1923 MB. 45.4908%. Relu + 1.16659 MB. 2.76513%. Add + 0.004 MB. 0.00948104%. FC + 42.1895 MB in Total +Parameter Memory per operator type: + 12.2576 MB. 70.5205%. Conv + 5.124 MB. 29.4795%. FC + 0 MB. 0%. Add + 0 MB. 0%. Relu + 17.3816 MB in Total +``` diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/LICENSE b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/LICENSE new file mode 100644 index 00000000000..80e7d155082 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/LICENSE @@ -0,0 +1,201 @@ + 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. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Ross Wightman + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/README.md b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/README.md new file mode 100644 index 00000000000..463368280d6 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/README.md @@ -0,0 +1,323 @@ +# (Generic) EfficientNets for PyTorch + +A 'generic' implementation of EfficientNet, MixNet, MobileNetV3, etc. that covers most of the compute/parameter efficient architectures derived from the MobileNet V1/V2 block sequence, including those found via automated neural architecture search. + +All models are implemented by GenEfficientNet or MobileNetV3 classes, with string based architecture definitions to configure the block layouts (idea from [here](https://github.com/tensorflow/tpu/blob/master/models/official/mnasnet/mnasnet_models.py)) + +## What's New + +### Aug 19, 2020 +* Add updated PyTorch trained EfficientNet-B3 weights trained by myself with `timm` (82.1 top-1) +* Add PyTorch trained EfficientNet-Lite0 contributed by [@hal-314](https://github.com/hal-314) (75.5 top-1) +* Update ONNX and Caffe2 export / utility scripts to work with latest PyTorch / ONNX +* ONNX runtime based validation script added +* activations (mostly) brought in sync with `timm` equivalents + + +### April 5, 2020 +* Add some newly trained MobileNet-V2 models trained with latest h-params, rand augment. They compare quite favourably to EfficientNet-Lite + * 3.5M param MobileNet-V2 100 @ 73% + * 4.5M param MobileNet-V2 110d @ 75% + * 6.1M param MobileNet-V2 140 @ 76.5% + * 5.8M param MobileNet-V2 120d @ 77.3% + +### March 23, 2020 + * Add EfficientNet-Lite models w/ weights ported from [Tensorflow TPU](https://github.com/tensorflow/tpu/tree/master/models/official/efficientnet/lite) + * Add PyTorch trained MobileNet-V3 Large weights with 75.77% top-1 + * IMPORTANT CHANGE (if training from scratch) - weight init changed to better match Tensorflow impl, set `fix_group_fanout=False` in `initialize_weight_goog` for old behavior + +### Feb 12, 2020 + * Add EfficientNet-L2 and B0-B7 NoisyStudent weights ported from [Tensorflow TPU](https://github.com/tensorflow/tpu/tree/master/models/official/efficientnet) + * Port new EfficientNet-B8 (RandAugment) weights from TF TPU, these are different than the B8 AdvProp, different input normalization. + * Add RandAugment PyTorch trained EfficientNet-ES (EdgeTPU-Small) weights with 78.1 top-1. Trained by [Andrew Lavin](https://github.com/andravin) + +### Jan 22, 2020 + * Update weights for EfficientNet B0, B2, B3 and MixNet-XL with latest RandAugment trained weights. Trained with (https://github.com/rwightman/pytorch-image-models) + * Fix torchscript compatibility for PyTorch 1.4, add torchscript support for MixedConv2d using ModuleDict + * Test models, torchscript, onnx export with PyTorch 1.4 -- no issues + +### Nov 22, 2019 + * New top-1 high! Ported official TF EfficientNet AdvProp (https://arxiv.org/abs/1911.09665) weights and B8 model spec. Created a new set of `ap` models since they use a different + preprocessing (Inception mean/std) from the original EfficientNet base/AA/RA weights. + +### Nov 15, 2019 + * Ported official TF MobileNet-V3 float32 large/small/minimalistic weights + * Modifications to MobileNet-V3 model and components to support some additional config needed for differences between TF MobileNet-V3 and mine + +### Oct 30, 2019 + * Many of the models will now work with torch.jit.script, MixNet being the biggest exception + * Improved interface for enabling torchscript or ONNX export compatible modes (via config) + * Add JIT optimized mem-efficient Swish/Mish autograd.fn in addition to memory-efficient autgrad.fn + * Activation factory to select best version of activation by name or override one globally + * Add pretrained checkpoint load helper that handles input conv and classifier changes + +### Oct 27, 2019 + * Add CondConv EfficientNet variants ported from https://github.com/tensorflow/tpu/tree/master/models/official/efficientnet/condconv + * Add RandAug weights for TF EfficientNet B5 and B7 from https://github.com/tensorflow/tpu/tree/master/models/official/efficientnet + * Bring over MixNet-XL model and depth scaling algo from my pytorch-image-models code base + * Switch activations and global pooling to modules + * Add memory-efficient Swish/Mish impl + * Add as_sequential() method to all models and allow as an argument in entrypoint fns + * Move MobileNetV3 into own file since it has a different head + * Remove ChamNet, MobileNet V2/V1 since they will likely never be used here + +## Models + +Implemented models include: + * EfficientNet NoisyStudent (B0-B7, L2) (https://arxiv.org/abs/1911.04252) + * EfficientNet AdvProp (B0-B8) (https://arxiv.org/abs/1911.09665) + * EfficientNet (B0-B8) (https://arxiv.org/abs/1905.11946) + * EfficientNet-EdgeTPU (S, M, L) (https://ai.googleblog.com/2019/08/efficientnet-edgetpu-creating.html) + * EfficientNet-CondConv (https://arxiv.org/abs/1904.04971) + * EfficientNet-Lite (https://github.com/tensorflow/tpu/tree/master/models/official/efficientnet/lite) + * MixNet (https://arxiv.org/abs/1907.09595) + * MNASNet B1, A1 (Squeeze-Excite), and Small (https://arxiv.org/abs/1807.11626) + * MobileNet-V3 (https://arxiv.org/abs/1905.02244) + * FBNet-C (https://arxiv.org/abs/1812.03443) + * Single-Path NAS (https://arxiv.org/abs/1904.02877) + +I originally implemented and trained some these models with code [here](https://github.com/rwightman/pytorch-image-models), this repository contains just the GenEfficientNet models, validation, and associated ONNX/Caffe2 export code. + +## Pretrained + +I've managed to train several of the models to accuracies close to or above the originating papers and official impl. My training code is here: https://github.com/rwightman/pytorch-image-models + + +|Model | Prec@1 (Err) | Prec@5 (Err) | Param#(M) | MAdds(M) | Image Scaling | Resolution | Crop | +|---|---|---|---|---|---|---|---| +| efficientnet_b3 | 82.240 (17.760) | 96.116 (3.884) | 12.23 | TBD | bicubic | 320 | 1.0 | +| efficientnet_b3 | 82.076 (17.924) | 96.020 (3.980) | 12.23 | TBD | bicubic | 300 | 0.904 | +| mixnet_xl | 81.074 (18.926) | 95.282 (4.718) | 11.90 | TBD | bicubic | 256 | 1.0 | +| efficientnet_b2 | 80.612 (19.388) | 95.318 (4.682) | 9.1 | TBD | bicubic | 288 | 1.0 | +| mixnet_xl | 80.476 (19.524) | 94.936 (5.064) | 11.90 | TBD | bicubic | 224 | 0.875 | +| efficientnet_b2 | 80.288 (19.712) | 95.166 (4.834) | 9.1 | 1003 | bicubic | 260 | 0.890 | +| mixnet_l | 78.976 (21.024 | 94.184 (5.816) | 7.33 | TBD | bicubic | 224 | 0.875 | +| efficientnet_b1 | 78.692 (21.308) | 94.086 (5.914) | 7.8 | 694 | bicubic | 240 | 0.882 | +| efficientnet_es | 78.066 (21.934) | 93.926 (6.074) | 5.44 | TBD | bicubic | 224 | 0.875 | +| efficientnet_b0 | 77.698 (22.302) | 93.532 (6.468) | 5.3 | 390 | bicubic | 224 | 0.875 | +| mobilenetv2_120d | 77.294 (22.706 | 93.502 (6.498) | 5.8 | TBD | bicubic | 224 | 0.875 | +| mixnet_m | 77.256 (22.744) | 93.418 (6.582) | 5.01 | 353 | bicubic | 224 | 0.875 | +| mobilenetv2_140 | 76.524 (23.476) | 92.990 (7.010) | 6.1 | TBD | bicubic | 224 | 0.875 | +| mixnet_s | 75.988 (24.012) | 92.794 (7.206) | 4.13 | TBD | bicubic | 224 | 0.875 | +| mobilenetv3_large_100 | 75.766 (24.234) | 92.542 (7.458) | 5.5 | TBD | bicubic | 224 | 0.875 | +| mobilenetv3_rw | 75.634 (24.366) | 92.708 (7.292) | 5.5 | 219 | bicubic | 224 | 0.875 | +| efficientnet_lite0 | 75.472 (24.528) | 92.520 (7.480) | 4.65 | TBD | bicubic | 224 | 0.875 | +| mnasnet_a1 | 75.448 (24.552) | 92.604 (7.396) | 3.9 | 312 | bicubic | 224 | 0.875 | +| fbnetc_100 | 75.124 (24.876) | 92.386 (7.614) | 5.6 | 385 | bilinear | 224 | 0.875 | +| mobilenetv2_110d | 75.052 (24.948) | 92.180 (7.820) | 4.5 | TBD | bicubic | 224 | 0.875 | +| mnasnet_b1 | 74.658 (25.342) | 92.114 (7.886) | 4.4 | 315 | bicubic | 224 | 0.875 | +| spnasnet_100 | 74.084 (25.916) | 91.818 (8.182) | 4.4 | TBD | bilinear | 224 | 0.875 | +| mobilenetv2_100 | 72.978 (27.022) | 91.016 (8.984) | 3.5 | TBD | bicubic | 224 | 0.875 | + + +More pretrained models to come... + + +## Ported Weights + +The weights ported from Tensorflow checkpoints for the EfficientNet models do pretty much match accuracy in Tensorflow once a SAME convolution padding equivalent is added, and the same crop factors, image scaling, etc (see table) are used via cmd line args. + +**IMPORTANT:** +* Tensorflow ported weights for EfficientNet AdvProp (AP), EfficientNet EdgeTPU, EfficientNet-CondConv, EfficientNet-Lite, and MobileNet-V3 models use Inception style (0.5, 0.5, 0.5) for mean and std. +* Enabling the Tensorflow preprocessing pipeline with `--tf-preprocessing` at validation time will improve scores by 0.1-0.5%, very close to original TF impl. + +To run validation for tf_efficientnet_b5: +`python validate.py /path/to/imagenet/validation/ --model tf_efficientnet_b5 -b 64 --img-size 456 --crop-pct 0.934 --interpolation bicubic` + +To run validation w/ TF preprocessing for tf_efficientnet_b5: +`python validate.py /path/to/imagenet/validation/ --model tf_efficientnet_b5 -b 64 --img-size 456 --tf-preprocessing` + +To run validation for a model with Inception preprocessing, ie EfficientNet-B8 AdvProp: +`python validate.py /path/to/imagenet/validation/ --model tf_efficientnet_b8_ap -b 48 --num-gpu 2 --img-size 672 --crop-pct 0.954 --mean 0.5 --std 0.5` + +|Model | Prec@1 (Err) | Prec@5 (Err) | Param # | Image Scaling | Image Size | Crop | +|---|---|---|---|---|---|---| +| tf_efficientnet_l2_ns *tfp | 88.352 (11.648) | 98.652 (1.348) | 480 | bicubic | 800 | N/A | +| tf_efficientnet_l2_ns | TBD | TBD | 480 | bicubic | 800 | 0.961 | +| tf_efficientnet_l2_ns_475 | 88.234 (11.766) | 98.546 (1.454) | 480 | bicubic | 475 | 0.936 | +| tf_efficientnet_l2_ns_475 *tfp | 88.172 (11.828) | 98.566 (1.434) | 480 | bicubic | 475 | N/A | +| tf_efficientnet_b7_ns *tfp | 86.844 (13.156) | 98.084 (1.916) | 66.35 | bicubic | 600 | N/A | +| tf_efficientnet_b7_ns | 86.840 (13.160) | 98.094 (1.906) | 66.35 | bicubic | 600 | N/A | +| tf_efficientnet_b6_ns | 86.452 (13.548) | 97.882 (2.118) | 43.04 | bicubic | 528 | N/A | +| tf_efficientnet_b6_ns *tfp | 86.444 (13.556) | 97.880 (2.120) | 43.04 | bicubic | 528 | N/A | +| tf_efficientnet_b5_ns *tfp | 86.064 (13.936) | 97.746 (2.254) | 30.39 | bicubic | 456 | N/A | +| tf_efficientnet_b5_ns | 86.088 (13.912) | 97.752 (2.248) | 30.39 | bicubic | 456 | N/A | +| tf_efficientnet_b8_ap *tfp | 85.436 (14.564) | 97.272 (2.728) | 87.4 | bicubic | 672 | N/A | +| tf_efficientnet_b8 *tfp | 85.384 (14.616) | 97.394 (2.606) | 87.4 | bicubic | 672 | N/A | +| tf_efficientnet_b8 | 85.370 (14.630) | 97.390 (2.610) | 87.4 | bicubic | 672 | 0.954 | +| tf_efficientnet_b8_ap | 85.368 (14.632) | 97.294 (2.706) | 87.4 | bicubic | 672 | 0.954 | +| tf_efficientnet_b4_ns *tfp | 85.298 (14.702) | 97.504 (2.496) | 19.34 | bicubic | 380 | N/A | +| tf_efficientnet_b4_ns | 85.162 (14.838) | 97.470 (2.530) | 19.34 | bicubic | 380 | 0.922 | +| tf_efficientnet_b7_ap *tfp | 85.154 (14.846) | 97.244 (2.756) | 66.35 | bicubic | 600 | N/A | +| tf_efficientnet_b7_ap | 85.118 (14.882) | 97.252 (2.748) | 66.35 | bicubic | 600 | 0.949 | +| tf_efficientnet_b7 *tfp | 84.940 (15.060) | 97.214 (2.786) | 66.35 | bicubic | 600 | N/A | +| tf_efficientnet_b7 | 84.932 (15.068) | 97.208 (2.792) | 66.35 | bicubic | 600 | 0.949 | +| tf_efficientnet_b6_ap | 84.786 (15.214) | 97.138 (2.862) | 43.04 | bicubic | 528 | 0.942 | +| tf_efficientnet_b6_ap *tfp | 84.760 (15.240) | 97.124 (2.876) | 43.04 | bicubic | 528 | N/A | +| tf_efficientnet_b5_ap *tfp | 84.276 (15.724) | 96.932 (3.068) | 30.39 | bicubic | 456 | N/A | +| tf_efficientnet_b5_ap | 84.254 (15.746) | 96.976 (3.024) | 30.39 | bicubic | 456 | 0.934 | +| tf_efficientnet_b6 *tfp | 84.140 (15.860) | 96.852 (3.148) | 43.04 | bicubic | 528 | N/A | +| tf_efficientnet_b6 | 84.110 (15.890) | 96.886 (3.114) | 43.04 | bicubic | 528 | 0.942 | +| tf_efficientnet_b3_ns *tfp | 84.054 (15.946) | 96.918 (3.082) | 12.23 | bicubic | 300 | N/A | +| tf_efficientnet_b3_ns | 84.048 (15.952) | 96.910 (3.090) | 12.23 | bicubic | 300 | .904 | +| tf_efficientnet_b5 *tfp | 83.822 (16.178) | 96.756 (3.244) | 30.39 | bicubic | 456 | N/A | +| tf_efficientnet_b5 | 83.812 (16.188) | 96.748 (3.252) | 30.39 | bicubic | 456 | 0.934 | +| tf_efficientnet_b4_ap *tfp | 83.278 (16.722) | 96.376 (3.624) | 19.34 | bicubic | 380 | N/A | +| tf_efficientnet_b4_ap | 83.248 (16.752) | 96.388 (3.612) | 19.34 | bicubic | 380 | 0.922 | +| tf_efficientnet_b4 | 83.022 (16.978) | 96.300 (3.700) | 19.34 | bicubic | 380 | 0.922 | +| tf_efficientnet_b4 *tfp | 82.948 (17.052) | 96.308 (3.692) | 19.34 | bicubic | 380 | N/A | +| tf_efficientnet_b2_ns *tfp | 82.436 (17.564) | 96.268 (3.732) | 9.11 | bicubic | 260 | N/A | +| tf_efficientnet_b2_ns | 82.380 (17.620) | 96.248 (3.752) | 9.11 | bicubic | 260 | 0.89 | +| tf_efficientnet_b3_ap *tfp | 81.882 (18.118) | 95.662 (4.338) | 12.23 | bicubic | 300 | N/A | +| tf_efficientnet_b3_ap | 81.828 (18.172) | 95.624 (4.376) | 12.23 | bicubic | 300 | 0.904 | +| tf_efficientnet_b3 | 81.636 (18.364) | 95.718 (4.282) | 12.23 | bicubic | 300 | 0.904 | +| tf_efficientnet_b3 *tfp | 81.576 (18.424) | 95.662 (4.338) | 12.23 | bicubic | 300 | N/A | +| tf_efficientnet_lite4 | 81.528 (18.472) | 95.668 (4.332) | 13.00 | bilinear | 380 | 0.92 | +| tf_efficientnet_b1_ns *tfp | 81.514 (18.486) | 95.776 (4.224) | 7.79 | bicubic | 240 | N/A | +| tf_efficientnet_lite4 *tfp | 81.502 (18.498) | 95.676 (4.324) | 13.00 | bilinear | 380 | N/A | +| tf_efficientnet_b1_ns | 81.388 (18.612) | 95.738 (4.262) | 7.79 | bicubic | 240 | 0.88 | +| tf_efficientnet_el | 80.534 (19.466) | 95.190 (4.810) | 10.59 | bicubic | 300 | 0.904 | +| tf_efficientnet_el *tfp | 80.476 (19.524) | 95.200 (4.800) | 10.59 | bicubic | 300 | N/A | +| tf_efficientnet_b2_ap *tfp | 80.420 (19.580) | 95.040 (4.960) | 9.11 | bicubic | 260 | N/A | +| tf_efficientnet_b2_ap | 80.306 (19.694) | 95.028 (4.972) | 9.11 | bicubic | 260 | 0.890 | +| tf_efficientnet_b2 *tfp | 80.188 (19.812) | 94.974 (5.026) | 9.11 | bicubic | 260 | N/A | +| tf_efficientnet_b2 | 80.086 (19.914) | 94.908 (5.092) | 9.11 | bicubic | 260 | 0.890 | +| tf_efficientnet_lite3 | 79.812 (20.188) | 94.914 (5.086) | 8.20 | bilinear | 300 | 0.904 | +| tf_efficientnet_lite3 *tfp | 79.734 (20.266) | 94.838 (5.162) | 8.20 | bilinear | 300 | N/A | +| tf_efficientnet_b1_ap *tfp | 79.532 (20.468) | 94.378 (5.622) | 7.79 | bicubic | 240 | N/A | +| tf_efficientnet_cc_b1_8e *tfp | 79.464 (20.536)| 94.492 (5.508) | 39.7 | bicubic | 240 | 0.88 | +| tf_efficientnet_cc_b1_8e | 79.298 (20.702) | 94.364 (5.636) | 39.7 | bicubic | 240 | 0.88 | +| tf_efficientnet_b1_ap | 79.278 (20.722) | 94.308 (5.692) | 7.79 | bicubic | 240 | 0.88 | +| tf_efficientnet_b1 *tfp | 79.172 (20.828) | 94.450 (5.550) | 7.79 | bicubic | 240 | N/A | +| tf_efficientnet_em *tfp | 78.958 (21.042) | 94.458 (5.542) | 6.90 | bicubic | 240 | N/A | +| tf_efficientnet_b0_ns *tfp | 78.806 (21.194) | 94.496 (5.504) | 5.29 | bicubic | 224 | N/A | +| tf_mixnet_l *tfp | 78.846 (21.154) | 94.212 (5.788) | 7.33 | bilinear | 224 | N/A | +| tf_efficientnet_b1 | 78.826 (21.174) | 94.198 (5.802) | 7.79 | bicubic | 240 | 0.88 | +| tf_mixnet_l | 78.770 (21.230) | 94.004 (5.996) | 7.33 | bicubic | 224 | 0.875 | +| tf_efficientnet_em | 78.742 (21.258) | 94.332 (5.668) | 6.90 | bicubic | 240 | 0.875 | +| tf_efficientnet_b0_ns | 78.658 (21.342) | 94.376 (5.624) | 5.29 | bicubic | 224 | 0.875 | +| tf_efficientnet_cc_b0_8e *tfp | 78.314 (21.686) | 93.790 (6.210) | 24.0 | bicubic | 224 | 0.875 | +| tf_efficientnet_cc_b0_8e | 77.908 (22.092) | 93.656 (6.344) | 24.0 | bicubic | 224 | 0.875 | +| tf_efficientnet_cc_b0_4e *tfp | 77.746 (22.254) | 93.552 (6.448) | 13.3 | bicubic | 224 | 0.875 | +| tf_efficientnet_cc_b0_4e | 77.304 (22.696) | 93.332 (6.668) | 13.3 | bicubic | 224 | 0.875 | +| tf_efficientnet_es *tfp | 77.616 (22.384) | 93.750 (6.250) | 5.44 | bicubic | 224 | N/A | +| tf_efficientnet_lite2 *tfp | 77.544 (22.456) | 93.800 (6.200) | 6.09 | bilinear | 260 | N/A | +| tf_efficientnet_lite2 | 77.460 (22.540) | 93.746 (6.254) | 6.09 | bicubic | 260 | 0.89 | +| tf_efficientnet_b0_ap *tfp | 77.514 (22.486) | 93.576 (6.424) | 5.29 | bicubic | 224 | N/A | +| tf_efficientnet_es | 77.264 (22.736) | 93.600 (6.400) | 5.44 | bicubic | 224 | N/A | +| tf_efficientnet_b0 *tfp | 77.258 (22.742) | 93.478 (6.522) | 5.29 | bicubic | 224 | N/A | +| tf_efficientnet_b0_ap | 77.084 (22.916) | 93.254 (6.746) | 5.29 | bicubic | 224 | 0.875 | +| tf_mixnet_m *tfp | 77.072 (22.928) | 93.368 (6.632) | 5.01 | bilinear | 224 | N/A | +| tf_mixnet_m | 76.950 (23.050) | 93.156 (6.844) | 5.01 | bicubic | 224 | 0.875 | +| tf_efficientnet_b0 | 76.848 (23.152) | 93.228 (6.772) | 5.29 | bicubic | 224 | 0.875 | +| tf_efficientnet_lite1 *tfp | 76.764 (23.236) | 93.326 (6.674) | 5.42 | bilinear | 240 | N/A | +| tf_efficientnet_lite1 | 76.638 (23.362) | 93.232 (6.768) | 5.42 | bicubic | 240 | 0.882 | +| tf_mixnet_s *tfp | 75.800 (24.200) | 92.788 (7.212) | 4.13 | bilinear | 224 | N/A | +| tf_mobilenetv3_large_100 *tfp | 75.768 (24.232) | 92.710 (7.290) | 5.48 | bilinear | 224 | N/A | +| tf_mixnet_s | 75.648 (24.352) | 92.636 (7.364) | 4.13 | bicubic | 224 | 0.875 | +| tf_mobilenetv3_large_100 | 75.516 (24.484) | 92.600 (7.400) | 5.48 | bilinear | 224 | 0.875 | +| tf_efficientnet_lite0 *tfp | 75.074 (24.926) | 92.314 (7.686) | 4.65 | bilinear | 224 | N/A | +| tf_efficientnet_lite0 | 74.842 (25.158) | 92.170 (7.830) | 4.65 | bicubic | 224 | 0.875 | +| tf_mobilenetv3_large_075 *tfp | 73.730 (26.270) | 91.616 (8.384) | 3.99 | bilinear | 224 |N/A | +| tf_mobilenetv3_large_075 | 73.442 (26.558) | 91.352 (8.648) | 3.99 | bilinear | 224 | 0.875 | +| tf_mobilenetv3_large_minimal_100 *tfp | 72.678 (27.322) | 90.860 (9.140) | 3.92 | bilinear | 224 | N/A | +| tf_mobilenetv3_large_minimal_100 | 72.244 (27.756) | 90.636 (9.364) | 3.92 | bilinear | 224 | 0.875 | +| tf_mobilenetv3_small_100 *tfp | 67.918 (32.082) | 87.958 (12.042 | 2.54 | bilinear | 224 | N/A | +| tf_mobilenetv3_small_100 | 67.918 (32.082) | 87.662 (12.338) | 2.54 | bilinear | 224 | 0.875 | +| tf_mobilenetv3_small_075 *tfp | 66.142 (33.858) | 86.498 (13.502) | 2.04 | bilinear | 224 | N/A | +| tf_mobilenetv3_small_075 | 65.718 (34.282) | 86.136 (13.864) | 2.04 | bilinear | 224 | 0.875 | +| tf_mobilenetv3_small_minimal_100 *tfp | 63.378 (36.622) | 84.802 (15.198) | 2.04 | bilinear | 224 | N/A | +| tf_mobilenetv3_small_minimal_100 | 62.898 (37.102) | 84.230 (15.770) | 2.04 | bilinear | 224 | 0.875 | + + +*tfp models validated with `tf-preprocessing` pipeline + +Google tf and tflite weights ported from official Tensorflow repositories +* https://github.com/tensorflow/tpu/tree/master/models/official/mnasnet +* https://github.com/tensorflow/tpu/tree/master/models/official/efficientnet +* https://github.com/tensorflow/models/tree/master/research/slim/nets/mobilenet + +## Usage + +### Environment + +All development and testing has been done in Conda Python 3 environments on Linux x86-64 systems, specifically Python 3.6.x, 3.7.x, 3.8.x. + +Users have reported that a Python 3 Anaconda install in Windows works. I have not verified this myself. + +PyTorch versions 1.4, 1.5, 1.6 have been tested with this code. + +I've tried to keep the dependencies minimal, the setup is as per the PyTorch default install instructions for Conda: +``` +conda create -n torch-env +conda activate torch-env +conda install -c pytorch pytorch torchvision cudatoolkit=10.2 +``` + +### PyTorch Hub + +Models can be accessed via the PyTorch Hub API + +``` +>>> torch.hub.list('rwightman/gen-efficientnet-pytorch') +['efficientnet_b0', ...] +>>> model = torch.hub.load('rwightman/gen-efficientnet-pytorch', 'efficientnet_b0', pretrained=True) +>>> model.eval() +>>> output = model(torch.randn(1,3,224,224)) +``` + +### Pip +This package can be installed via pip. + +Install (after conda env/install): +``` +pip install geffnet +``` + +Eval use: +``` +>>> import geffnet +>>> m = geffnet.create_model('mobilenetv3_large_100', pretrained=True) +>>> m.eval() +``` + +Train use: +``` +>>> import geffnet +>>> # models can also be created by using the entrypoint directly +>>> m = geffnet.efficientnet_b2(pretrained=True, drop_rate=0.25, drop_connect_rate=0.2) +>>> m.train() +``` + +Create in a nn.Sequential container, for fast.ai, etc: +``` +>>> import geffnet +>>> m = geffnet.mixnet_l(pretrained=True, drop_rate=0.25, drop_connect_rate=0.2, as_sequential=True) +``` + +### Exporting + +Scripts are included to +* export models to ONNX (`onnx_export.py`) +* optimized ONNX graph (`onnx_optimize.py` or `onnx_validate.py` w/ `--onnx-output-opt` arg) +* validate with ONNX runtime (`onnx_validate.py`) +* convert ONNX model to Caffe2 (`onnx_to_caffe.py`) +* validate in Caffe2 (`caffe2_validate.py`) +* benchmark in Caffe2 w/ FLOPs, parameters output (`caffe2_benchmark.py`) + +As an example, to export the MobileNet-V3 pretrained model and then run an Imagenet validation: +``` +python onnx_export.py --model mobilenetv3_large_100 ./mobilenetv3_100.onnx +python onnx_validate.py /imagenet/validation/ --onnx-input ./mobilenetv3_100.onnx +``` + +These scripts were tested to be working as of PyTorch 1.6 and ONNX 1.7 w/ ONNX runtime 1.4. Caffe2 compatible +export now requires additional args mentioned in the export script (not needed in earlier versions). + +#### Export Notes +1. The TF ported weights with the 'SAME' conv padding activated cannot be exported to ONNX unless `_EXPORTABLE` flag in `config.py` is set to True. Use `config.set_exportable(True)` as in the `onnx_export.py` script. +2. TF ported models with 'SAME' padding will have the padding fixed at export time to the resolution used for export. Even though dynamic padding is supported in opset >= 11, I can't get it working. +3. ONNX optimize facility doesn't work reliably in PyTorch 1.6 / ONNX 1.7. Fortunately, the onnxruntime based inference is working very well now and includes on the fly optimization. +3. ONNX / Caffe2 export/import frequently breaks with different PyTorch and ONNX version releases. Please check their respective issue trackers before filing issues here. + + diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/__init__.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/caffe2_benchmark.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/caffe2_benchmark.py new file mode 100644 index 00000000000..93f28a1e63d --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/caffe2_benchmark.py @@ -0,0 +1,65 @@ +""" Caffe2 validation script + +This script runs Caffe2 benchmark on exported ONNX model. +It is a useful tool for reporting model FLOPS. + +Copyright 2020 Ross Wightman +""" +import argparse +from caffe2.python import core, workspace, model_helper +from caffe2.proto import caffe2_pb2 + + +parser = argparse.ArgumentParser(description='Caffe2 Model Benchmark') +parser.add_argument('--c2-prefix', default='', type=str, metavar='NAME', + help='caffe2 model pb name prefix') +parser.add_argument('--c2-init', default='', type=str, metavar='PATH', + help='caffe2 model init .pb') +parser.add_argument('--c2-predict', default='', type=str, metavar='PATH', + help='caffe2 model predict .pb') +parser.add_argument('-b', '--batch-size', default=1, type=int, + metavar='N', help='mini-batch size (default: 1)') +parser.add_argument('--img-size', default=224, type=int, + metavar='N', help='Input image dimension, uses model default if empty') + + +def main(): + args = parser.parse_args() + args.gpu_id = 0 + if args.c2_prefix: + args.c2_init = args.c2_prefix + '.init.pb' + args.c2_predict = args.c2_prefix + '.predict.pb' + + model = model_helper.ModelHelper(name="le_net", init_params=False) + + # Bring in the init net from init_net.pb + init_net_proto = caffe2_pb2.NetDef() + with open(args.c2_init, "rb") as f: + init_net_proto.ParseFromString(f.read()) + model.param_init_net = core.Net(init_net_proto) + + # bring in the predict net from predict_net.pb + predict_net_proto = caffe2_pb2.NetDef() + with open(args.c2_predict, "rb") as f: + predict_net_proto.ParseFromString(f.read()) + model.net = core.Net(predict_net_proto) + + # CUDA performance not impressive + #device_opts = core.DeviceOption(caffe2_pb2.PROTO_CUDA, args.gpu_id) + #model.net.RunAllOnGPU(gpu_id=args.gpu_id, use_cudnn=True) + #model.param_init_net.RunAllOnGPU(gpu_id=args.gpu_id, use_cudnn=True) + + input_blob = model.net.external_inputs[0] + model.param_init_net.GaussianFill( + [], + input_blob.GetUnscopedName(), + shape=(args.batch_size, 3, args.img_size, args.img_size), + mean=0.0, + std=1.0) + workspace.RunNetOnce(model.param_init_net) + workspace.CreateNet(model.net, overwrite=True) + workspace.BenchmarkNet(model.net.Proto().name, 5, 20, True) + + +if __name__ == '__main__': + main() diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/caffe2_validate.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/caffe2_validate.py new file mode 100644 index 00000000000..7cfaab38c09 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/caffe2_validate.py @@ -0,0 +1,138 @@ +""" Caffe2 validation script + +This script is created to verify exported ONNX models running in Caffe2 +It utilizes the same PyTorch dataloader/processing pipeline for a +fair comparison against the originals. + +Copyright 2020 Ross Wightman +""" +import argparse +import numpy as np +from caffe2.python import core, workspace, model_helper +from caffe2.proto import caffe2_pb2 +from data import create_loader, resolve_data_config, Dataset +from utils import AverageMeter +import time + +parser = argparse.ArgumentParser(description='Caffe2 ImageNet Validation') +parser.add_argument('data', metavar='DIR', + help='path to dataset') +parser.add_argument('--c2-prefix', default='', type=str, metavar='NAME', + help='caffe2 model pb name prefix') +parser.add_argument('--c2-init', default='', type=str, metavar='PATH', + help='caffe2 model init .pb') +parser.add_argument('--c2-predict', default='', type=str, metavar='PATH', + help='caffe2 model predict .pb') +parser.add_argument('-j', '--workers', default=2, type=int, metavar='N', + help='number of data loading workers (default: 2)') +parser.add_argument('-b', '--batch-size', default=256, type=int, + metavar='N', help='mini-batch size (default: 256)') +parser.add_argument('--img-size', default=None, type=int, + metavar='N', help='Input image dimension, uses model default if empty') +parser.add_argument('--mean', type=float, nargs='+', default=None, metavar='MEAN', + help='Override mean pixel value of dataset') +parser.add_argument('--std', type=float, nargs='+', default=None, metavar='STD', + help='Override std deviation of of dataset') +parser.add_argument('--crop-pct', type=float, default=None, metavar='PCT', + help='Override default crop pct of 0.875') +parser.add_argument('--interpolation', default='', type=str, metavar='NAME', + help='Image resize interpolation type (overrides model)') +parser.add_argument('--tf-preprocessing', dest='tf_preprocessing', action='store_true', + help='use tensorflow mnasnet preporcessing') +parser.add_argument('--print-freq', '-p', default=10, type=int, + metavar='N', help='print frequency (default: 10)') + + +def main(): + args = parser.parse_args() + args.gpu_id = 0 + if args.c2_prefix: + args.c2_init = args.c2_prefix + '.init.pb' + args.c2_predict = args.c2_prefix + '.predict.pb' + + model = model_helper.ModelHelper(name="validation_net", init_params=False) + + # Bring in the init net from init_net.pb + init_net_proto = caffe2_pb2.NetDef() + with open(args.c2_init, "rb") as f: + init_net_proto.ParseFromString(f.read()) + model.param_init_net = core.Net(init_net_proto) + + # bring in the predict net from predict_net.pb + predict_net_proto = caffe2_pb2.NetDef() + with open(args.c2_predict, "rb") as f: + predict_net_proto.ParseFromString(f.read()) + model.net = core.Net(predict_net_proto) + + data_config = resolve_data_config(None, args) + loader = create_loader( + Dataset(args.data, load_bytes=args.tf_preprocessing), + input_size=data_config['input_size'], + batch_size=args.batch_size, + use_prefetcher=False, + interpolation=data_config['interpolation'], + mean=data_config['mean'], + std=data_config['std'], + num_workers=args.workers, + crop_pct=data_config['crop_pct'], + tensorflow_preprocessing=args.tf_preprocessing) + + # this is so obvious, wonderful interface + input_blob = model.net.external_inputs[0] + output_blob = model.net.external_outputs[0] + + if True: + device_opts = None + else: + # CUDA is crashing, no idea why, awesome error message, give it a try for kicks + device_opts = core.DeviceOption(caffe2_pb2.PROTO_CUDA, args.gpu_id) + model.net.RunAllOnGPU(gpu_id=args.gpu_id, use_cudnn=True) + model.param_init_net.RunAllOnGPU(gpu_id=args.gpu_id, use_cudnn=True) + + model.param_init_net.GaussianFill( + [], input_blob.GetUnscopedName(), + shape=(1,) + data_config['input_size'], mean=0.0, std=1.0) + workspace.RunNetOnce(model.param_init_net) + workspace.CreateNet(model.net, overwrite=True) + + batch_time = AverageMeter() + top1 = AverageMeter() + top5 = AverageMeter() + end = time.time() + for i, (input, target) in enumerate(loader): + # run the net and return prediction + caffe2_in = input.data.numpy() + workspace.FeedBlob(input_blob, caffe2_in, device_opts) + workspace.RunNet(model.net, num_iter=1) + output = workspace.FetchBlob(output_blob) + + # measure accuracy and record loss + prec1, prec5 = accuracy_np(output.data, target.numpy()) + top1.update(prec1.item(), input.size(0)) + top5.update(prec5.item(), input.size(0)) + + # measure elapsed time + batch_time.update(time.time() - end) + end = time.time() + + if i % args.print_freq == 0: + print('Test: [{0}/{1}]\t' + 'Time {batch_time.val:.3f} ({batch_time.avg:.3f}, {rate_avg:.3f}/s, {ms_avg:.3f} ms/sample) \t' + 'Prec@1 {top1.val:.3f} ({top1.avg:.3f})\t' + 'Prec@5 {top5.val:.3f} ({top5.avg:.3f})'.format( + i, len(loader), batch_time=batch_time, rate_avg=input.size(0) / batch_time.avg, + ms_avg=100 * batch_time.avg / input.size(0), top1=top1, top5=top5)) + + print(' * Prec@1 {top1.avg:.3f} ({top1a:.3f}) Prec@5 {top5.avg:.3f} ({top5a:.3f})'.format( + top1=top1, top1a=100-top1.avg, top5=top5, top5a=100.-top5.avg)) + + +def accuracy_np(output, target): + max_indices = np.argsort(output, axis=1)[:, ::-1] + top5 = 100 * np.equal(max_indices[:, :5], target[:, np.newaxis]).sum(axis=1).mean() + top1 = 100 * np.equal(max_indices[:, 0], target).mean() + return top1, top5 + + +if __name__ == '__main__': + main() diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/__init__.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/__init__.py new file mode 100644 index 00000000000..2e441a5838d --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/__init__.py @@ -0,0 +1,5 @@ +from .gen_efficientnet import * +from .mobilenetv3 import * +from .model_factory import create_model +from .config import is_exportable, is_scriptable, set_exportable, set_scriptable +from .activations import * \ No newline at end of file diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/activations/__init__.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/activations/__init__.py new file mode 100644 index 00000000000..813421a743f --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/activations/__init__.py @@ -0,0 +1,137 @@ +from geffnet import config +from geffnet.activations.activations_me import * +from geffnet.activations.activations_jit import * +from geffnet.activations.activations import * +import torch + +_has_silu = 'silu' in dir(torch.nn.functional) + +_ACT_FN_DEFAULT = dict( + silu=F.silu if _has_silu else swish, + swish=F.silu if _has_silu else swish, + mish=mish, + relu=F.relu, + relu6=F.relu6, + sigmoid=sigmoid, + tanh=tanh, + hard_sigmoid=hard_sigmoid, + hard_swish=hard_swish, +) + +_ACT_FN_JIT = dict( + silu=F.silu if _has_silu else swish_jit, + swish=F.silu if _has_silu else swish_jit, + mish=mish_jit, +) + +_ACT_FN_ME = dict( + silu=F.silu if _has_silu else swish_me, + swish=F.silu if _has_silu else swish_me, + mish=mish_me, + hard_swish=hard_swish_me, + hard_sigmoid_jit=hard_sigmoid_me, +) + +_ACT_LAYER_DEFAULT = dict( + silu=nn.SiLU if _has_silu else Swish, + swish=nn.SiLU if _has_silu else Swish, + mish=Mish, + relu=nn.ReLU, + relu6=nn.ReLU6, + sigmoid=Sigmoid, + tanh=Tanh, + hard_sigmoid=HardSigmoid, + hard_swish=HardSwish, +) + +_ACT_LAYER_JIT = dict( + silu=nn.SiLU if _has_silu else SwishJit, + swish=nn.SiLU if _has_silu else SwishJit, + mish=MishJit, +) + +_ACT_LAYER_ME = dict( + silu=nn.SiLU if _has_silu else SwishMe, + swish=nn.SiLU if _has_silu else SwishMe, + mish=MishMe, + hard_swish=HardSwishMe, + hard_sigmoid=HardSigmoidMe +) + +_OVERRIDE_FN = dict() +_OVERRIDE_LAYER = dict() + + +def add_override_act_fn(name, fn): + global _OVERRIDE_FN + _OVERRIDE_FN[name] = fn + + +def update_override_act_fn(overrides): + assert isinstance(overrides, dict) + global _OVERRIDE_FN + _OVERRIDE_FN.update(overrides) + + +def clear_override_act_fn(): + global _OVERRIDE_FN + _OVERRIDE_FN = dict() + + +def add_override_act_layer(name, fn): + _OVERRIDE_LAYER[name] = fn + + +def update_override_act_layer(overrides): + assert isinstance(overrides, dict) + global _OVERRIDE_LAYER + _OVERRIDE_LAYER.update(overrides) + + +def clear_override_act_layer(): + global _OVERRIDE_LAYER + _OVERRIDE_LAYER = dict() + + +def get_act_fn(name='relu'): + """ Activation Function Factory + Fetching activation fns by name with this function allows export or torch script friendly + functions to be returned dynamically based on current config. + """ + if name in _OVERRIDE_FN: + return _OVERRIDE_FN[name] + use_me = not (config.is_exportable() or config.is_scriptable() or config.is_no_jit()) + if use_me and name in _ACT_FN_ME: + # If not exporting or scripting the model, first look for a memory optimized version + # activation with custom autograd, then fallback to jit scripted, then a Python or Torch builtin + return _ACT_FN_ME[name] + if config.is_exportable() and name in ('silu', 'swish'): + # FIXME PyTorch SiLU doesn't ONNX export, this is a temp hack + return swish + use_jit = not (config.is_exportable() or config.is_no_jit()) + # NOTE: export tracing should work with jit scripted components, but I keep running into issues + if use_jit and name in _ACT_FN_JIT: # jit scripted models should be okay for export/scripting + return _ACT_FN_JIT[name] + return _ACT_FN_DEFAULT[name] + + +def get_act_layer(name='relu'): + """ Activation Layer Factory + Fetching activation layers by name with this function allows export or torch script friendly + functions to be returned dynamically based on current config. + """ + if name in _OVERRIDE_LAYER: + return _OVERRIDE_LAYER[name] + use_me = not (config.is_exportable() or config.is_scriptable() or config.is_no_jit()) + if use_me and name in _ACT_LAYER_ME: + return _ACT_LAYER_ME[name] + if config.is_exportable() and name in ('silu', 'swish'): + # FIXME PyTorch SiLU doesn't ONNX export, this is a temp hack + return Swish + use_jit = not (config.is_exportable() or config.is_no_jit()) + # NOTE: export tracing should work with jit scripted components, but I keep running into issues + if use_jit and name in _ACT_FN_JIT: # jit scripted models should be okay for export/scripting + return _ACT_LAYER_JIT[name] + return _ACT_LAYER_DEFAULT[name] + + diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/activations/activations.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/activations/activations.py new file mode 100644 index 00000000000..bdea692d139 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/activations/activations.py @@ -0,0 +1,102 @@ +""" Activations + +A collection of activations fn and modules with a common interface so that they can +easily be swapped. All have an `inplace` arg even if not used. + +Copyright 2020 Ross Wightman +""" +from torch import nn as nn +from torch.nn import functional as F + + +def swish(x, inplace: bool = False): + """Swish - Described originally as SiLU (https://arxiv.org/abs/1702.03118v3) + and also as Swish (https://arxiv.org/abs/1710.05941). + + TODO Rename to SiLU with addition to PyTorch + """ + return x.mul_(x.sigmoid()) if inplace else x.mul(x.sigmoid()) + + +class Swish(nn.Module): + def __init__(self, inplace: bool = False): + super(Swish, self).__init__() + self.inplace = inplace + + def forward(self, x): + return swish(x, self.inplace) + + +def mish(x, inplace: bool = False): + """Mish: A Self Regularized Non-Monotonic Neural Activation Function - https://arxiv.org/abs/1908.08681 + """ + return x.mul(F.softplus(x).tanh()) + + +class Mish(nn.Module): + def __init__(self, inplace: bool = False): + super(Mish, self).__init__() + self.inplace = inplace + + def forward(self, x): + return mish(x, self.inplace) + + +def sigmoid(x, inplace: bool = False): + return x.sigmoid_() if inplace else x.sigmoid() + + +# PyTorch has this, but not with a consistent inplace argmument interface +class Sigmoid(nn.Module): + def __init__(self, inplace: bool = False): + super(Sigmoid, self).__init__() + self.inplace = inplace + + def forward(self, x): + return x.sigmoid_() if self.inplace else x.sigmoid() + + +def tanh(x, inplace: bool = False): + return x.tanh_() if inplace else x.tanh() + + +# PyTorch has this, but not with a consistent inplace argmument interface +class Tanh(nn.Module): + def __init__(self, inplace: bool = False): + super(Tanh, self).__init__() + self.inplace = inplace + + def forward(self, x): + return x.tanh_() if self.inplace else x.tanh() + + +def hard_swish(x, inplace: bool = False): + inner = F.relu6(x + 3.).div_(6.) + return x.mul_(inner) if inplace else x.mul(inner) + + +class HardSwish(nn.Module): + def __init__(self, inplace: bool = False): + super(HardSwish, self).__init__() + self.inplace = inplace + + def forward(self, x): + return hard_swish(x, self.inplace) + + +def hard_sigmoid(x, inplace: bool = False): + if inplace: + return x.add_(3.).clamp_(0., 6.).div_(6.) + else: + return F.relu6(x + 3.) / 6. + + +class HardSigmoid(nn.Module): + def __init__(self, inplace: bool = False): + super(HardSigmoid, self).__init__() + self.inplace = inplace + + def forward(self, x): + return hard_sigmoid(x, self.inplace) + + diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/activations/activations_jit.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/activations/activations_jit.py new file mode 100644 index 00000000000..7176b05e779 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/activations/activations_jit.py @@ -0,0 +1,79 @@ +""" Activations (jit) + +A collection of jit-scripted activations fn and modules with a common interface so that they can +easily be swapped. All have an `inplace` arg even if not used. + +All jit scripted activations are lacking in-place variations on purpose, scripted kernel fusion does not +currently work across in-place op boundaries, thus performance is equal to or less than the non-scripted +versions if they contain in-place ops. + +Copyright 2020 Ross Wightman +""" + +import torch +from torch import nn as nn +from torch.nn import functional as F + +__all__ = ['swish_jit', 'SwishJit', 'mish_jit', 'MishJit', + 'hard_sigmoid_jit', 'HardSigmoidJit', 'hard_swish_jit', 'HardSwishJit'] + + +@torch.jit.script +def swish_jit(x, inplace: bool = False): + """Swish - Described originally as SiLU (https://arxiv.org/abs/1702.03118v3) + and also as Swish (https://arxiv.org/abs/1710.05941). + + TODO Rename to SiLU with addition to PyTorch + """ + return x.mul(x.sigmoid()) + + +@torch.jit.script +def mish_jit(x, _inplace: bool = False): + """Mish: A Self Regularized Non-Monotonic Neural Activation Function - https://arxiv.org/abs/1908.08681 + """ + return x.mul(F.softplus(x).tanh()) + + +class SwishJit(nn.Module): + def __init__(self, inplace: bool = False): + super(SwishJit, self).__init__() + + def forward(self, x): + return swish_jit(x) + + +class MishJit(nn.Module): + def __init__(self, inplace: bool = False): + super(MishJit, self).__init__() + + def forward(self, x): + return mish_jit(x) + + +@torch.jit.script +def hard_sigmoid_jit(x, inplace: bool = False): + # return F.relu6(x + 3.) / 6. + return (x + 3).clamp(min=0, max=6).div(6.) # clamp seems ever so slightly faster? + + +class HardSigmoidJit(nn.Module): + def __init__(self, inplace: bool = False): + super(HardSigmoidJit, self).__init__() + + def forward(self, x): + return hard_sigmoid_jit(x) + + +@torch.jit.script +def hard_swish_jit(x, inplace: bool = False): + # return x * (F.relu6(x + 3.) / 6) + return x * (x + 3).clamp(min=0, max=6).div(6.) # clamp seems ever so slightly faster? + + +class HardSwishJit(nn.Module): + def __init__(self, inplace: bool = False): + super(HardSwishJit, self).__init__() + + def forward(self, x): + return hard_swish_jit(x) diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/activations/activations_me.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/activations/activations_me.py new file mode 100644 index 00000000000..e91df5a50fd --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/activations/activations_me.py @@ -0,0 +1,174 @@ +""" Activations (memory-efficient w/ custom autograd) + +A collection of activations fn and modules with a common interface so that they can +easily be swapped. All have an `inplace` arg even if not used. + +These activations are not compatible with jit scripting or ONNX export of the model, please use either +the JIT or basic versions of the activations. + +Copyright 2020 Ross Wightman +""" + +import torch +from torch import nn as nn +from torch.nn import functional as F + + +__all__ = ['swish_me', 'SwishMe', 'mish_me', 'MishMe', + 'hard_sigmoid_me', 'HardSigmoidMe', 'hard_swish_me', 'HardSwishMe'] + + +@torch.jit.script +def swish_jit_fwd(x): + return x.mul(torch.sigmoid(x)) + + +@torch.jit.script +def swish_jit_bwd(x, grad_output): + x_sigmoid = torch.sigmoid(x) + return grad_output * (x_sigmoid * (1 + x * (1 - x_sigmoid))) + + +class SwishJitAutoFn(torch.autograd.Function): + """ torch.jit.script optimised Swish w/ memory-efficient checkpoint + Inspired by conversation btw Jeremy Howard & Adam Pazske + https://twitter.com/jeremyphoward/status/1188251041835315200 + + Swish - Described originally as SiLU (https://arxiv.org/abs/1702.03118v3) + and also as Swish (https://arxiv.org/abs/1710.05941). + + TODO Rename to SiLU with addition to PyTorch + """ + + @staticmethod + def forward(ctx, x): + ctx.save_for_backward(x) + return swish_jit_fwd(x) + + @staticmethod + def backward(ctx, grad_output): + x = ctx.saved_tensors[0] + return swish_jit_bwd(x, grad_output) + + +def swish_me(x, inplace=False): + return SwishJitAutoFn.apply(x) + + +class SwishMe(nn.Module): + def __init__(self, inplace: bool = False): + super(SwishMe, self).__init__() + + def forward(self, x): + return SwishJitAutoFn.apply(x) + + +@torch.jit.script +def mish_jit_fwd(x): + return x.mul(torch.tanh(F.softplus(x))) + + +@torch.jit.script +def mish_jit_bwd(x, grad_output): + x_sigmoid = torch.sigmoid(x) + x_tanh_sp = F.softplus(x).tanh() + return grad_output.mul(x_tanh_sp + x * x_sigmoid * (1 - x_tanh_sp * x_tanh_sp)) + + +class MishJitAutoFn(torch.autograd.Function): + """ Mish: A Self Regularized Non-Monotonic Neural Activation Function - https://arxiv.org/abs/1908.08681 + A memory efficient, jit scripted variant of Mish + """ + @staticmethod + def forward(ctx, x): + ctx.save_for_backward(x) + return mish_jit_fwd(x) + + @staticmethod + def backward(ctx, grad_output): + x = ctx.saved_tensors[0] + return mish_jit_bwd(x, grad_output) + + +def mish_me(x, inplace=False): + return MishJitAutoFn.apply(x) + + +class MishMe(nn.Module): + def __init__(self, inplace: bool = False): + super(MishMe, self).__init__() + + def forward(self, x): + return MishJitAutoFn.apply(x) + + +@torch.jit.script +def hard_sigmoid_jit_fwd(x, inplace: bool = False): + return (x + 3).clamp(min=0, max=6).div(6.) + + +@torch.jit.script +def hard_sigmoid_jit_bwd(x, grad_output): + m = torch.ones_like(x) * ((x >= -3.) & (x <= 3.)) / 6. + return grad_output * m + + +class HardSigmoidJitAutoFn(torch.autograd.Function): + @staticmethod + def forward(ctx, x): + ctx.save_for_backward(x) + return hard_sigmoid_jit_fwd(x) + + @staticmethod + def backward(ctx, grad_output): + x = ctx.saved_tensors[0] + return hard_sigmoid_jit_bwd(x, grad_output) + + +def hard_sigmoid_me(x, inplace: bool = False): + return HardSigmoidJitAutoFn.apply(x) + + +class HardSigmoidMe(nn.Module): + def __init__(self, inplace: bool = False): + super(HardSigmoidMe, self).__init__() + + def forward(self, x): + return HardSigmoidJitAutoFn.apply(x) + + +@torch.jit.script +def hard_swish_jit_fwd(x): + return x * (x + 3).clamp(min=0, max=6).div(6.) + + +@torch.jit.script +def hard_swish_jit_bwd(x, grad_output): + m = torch.ones_like(x) * (x >= 3.) + m = torch.where((x >= -3.) & (x <= 3.), x / 3. + .5, m) + return grad_output * m + + +class HardSwishJitAutoFn(torch.autograd.Function): + """A memory efficient, jit-scripted HardSwish activation""" + @staticmethod + def forward(ctx, x): + ctx.save_for_backward(x) + return hard_swish_jit_fwd(x) + + @staticmethod + def backward(ctx, grad_output): + x = ctx.saved_tensors[0] + return hard_swish_jit_bwd(x, grad_output) + + +def hard_swish_me(x, inplace=False): + return HardSwishJitAutoFn.apply(x) + + +class HardSwishMe(nn.Module): + def __init__(self, inplace: bool = False): + super(HardSwishMe, self).__init__() + + def forward(self, x): + return HardSwishJitAutoFn.apply(x) diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/config.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/config.py new file mode 100644 index 00000000000..27d5307fd9e --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/config.py @@ -0,0 +1,123 @@ +""" Global layer config state +""" +from typing import Any, Optional + +__all__ = [ + 'is_exportable', 'is_scriptable', 'is_no_jit', 'layer_config_kwargs', + 'set_exportable', 'set_scriptable', 'set_no_jit', 'set_layer_config' +] + +# Set to True if prefer to have layers with no jit optimization (includes activations) +_NO_JIT = False + +# Set to True if prefer to have activation layers with no jit optimization +# NOTE not currently used as no difference between no_jit and no_activation jit as only layers obeying +# the jit flags so far are activations. This will change as more layers are updated and/or added. +_NO_ACTIVATION_JIT = False + +# Set to True if exporting a model with Same padding via ONNX +_EXPORTABLE = False + +# Set to True if wanting to use torch.jit.script on a model +_SCRIPTABLE = False + + +def is_no_jit(): + return _NO_JIT + + +class set_no_jit: + def __init__(self, mode: bool) -> None: + global _NO_JIT + self.prev = _NO_JIT + _NO_JIT = mode + + def __enter__(self) -> None: + pass + + def __exit__(self, *args: Any) -> bool: + global _NO_JIT + _NO_JIT = self.prev + return False + + +def is_exportable(): + return _EXPORTABLE + + +class set_exportable: + def __init__(self, mode: bool) -> None: + global _EXPORTABLE + self.prev = _EXPORTABLE + _EXPORTABLE = mode + + def __enter__(self) -> None: + pass + + def __exit__(self, *args: Any) -> bool: + global _EXPORTABLE + _EXPORTABLE = self.prev + return False + + +def is_scriptable(): + return _SCRIPTABLE + + +class set_scriptable: + def __init__(self, mode: bool) -> None: + global _SCRIPTABLE + self.prev = _SCRIPTABLE + _SCRIPTABLE = mode + + def __enter__(self) -> None: + pass + + def __exit__(self, *args: Any) -> bool: + global _SCRIPTABLE + _SCRIPTABLE = self.prev + return False + + +class set_layer_config: + """ Layer config context manager that allows setting all layer config flags at once. + If a flag arg is None, it will not change the current value. + """ + def __init__( + self, + scriptable: Optional[bool] = None, + exportable: Optional[bool] = None, + no_jit: Optional[bool] = None, + no_activation_jit: Optional[bool] = None): + global _SCRIPTABLE + global _EXPORTABLE + global _NO_JIT + global _NO_ACTIVATION_JIT + self.prev = _SCRIPTABLE, _EXPORTABLE, _NO_JIT, _NO_ACTIVATION_JIT + if scriptable is not None: + _SCRIPTABLE = scriptable + if exportable is not None: + _EXPORTABLE = exportable + if no_jit is not None: + _NO_JIT = no_jit + if no_activation_jit is not None: + _NO_ACTIVATION_JIT = no_activation_jit + + def __enter__(self) -> None: + pass + + def __exit__(self, *args: Any) -> bool: + global _SCRIPTABLE + global _EXPORTABLE + global _NO_JIT + global _NO_ACTIVATION_JIT + _SCRIPTABLE, _EXPORTABLE, _NO_JIT, _NO_ACTIVATION_JIT = self.prev + return False + + +def layer_config_kwargs(kwargs): + """ Consume config kwargs and return contextmgr obj """ + return set_layer_config( + scriptable=kwargs.pop('scriptable', None), + exportable=kwargs.pop('exportable', None), + no_jit=kwargs.pop('no_jit', None)) diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/conv2d_layers.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/conv2d_layers.py new file mode 100644 index 00000000000..2369f7de2c3 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/conv2d_layers.py @@ -0,0 +1,304 @@ +""" Conv2D w/ SAME padding, CondConv, MixedConv + +A collection of conv layers and padding helpers needed by EfficientNet, MixNet, and +MobileNetV3 models that maintain weight compatibility with original Tensorflow models. + +Copyright 2020 Ross Wightman +""" +import collections.abc +import math +from functools import partial +from itertools import repeat +from typing import Tuple, Optional + +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F + +from .config import is_exportable, is_scriptable + + +# From PyTorch internals +def _ntuple(n): + def parse(x): + if isinstance(x, collections.abc.Iterable): + return x + return tuple(repeat(x, n)) + return parse + + +_single = _ntuple(1) +_pair = _ntuple(2) +_triple = _ntuple(3) +_quadruple = _ntuple(4) + + +def _is_static_pad(kernel_size, stride=1, dilation=1, **_): + return stride == 1 and (dilation * (kernel_size - 1)) % 2 == 0 + + +def _get_padding(kernel_size, stride=1, dilation=1, **_): + padding = ((stride - 1) + dilation * (kernel_size - 1)) // 2 + return padding + + +def _calc_same_pad(i: int, k: int, s: int, d: int): + return max((-(i // -s) - 1) * s + (k - 1) * d + 1 - i, 0) + + +def _same_pad_arg(input_size, kernel_size, stride, dilation): + ih, iw = input_size + kh, kw = kernel_size + pad_h = _calc_same_pad(ih, kh, stride[0], dilation[0]) + pad_w = _calc_same_pad(iw, kw, stride[1], dilation[1]) + return [pad_w // 2, pad_w - pad_w // 2, pad_h // 2, pad_h - pad_h // 2] + + +def _split_channels(num_chan, num_groups): + split = [num_chan // num_groups for _ in range(num_groups)] + split[0] += num_chan - sum(split) + return split + + +def conv2d_same( + x, weight: torch.Tensor, bias: Optional[torch.Tensor] = None, stride: Tuple[int, int] = (1, 1), + padding: Tuple[int, int] = (0, 0), dilation: Tuple[int, int] = (1, 1), groups: int = 1): + ih, iw = x.size()[-2:] + kh, kw = weight.size()[-2:] + pad_h = _calc_same_pad(ih, kh, stride[0], dilation[0]) + pad_w = _calc_same_pad(iw, kw, stride[1], dilation[1]) + x = F.pad(x, [pad_w // 2, pad_w - pad_w // 2, pad_h // 2, pad_h - pad_h // 2]) + return F.conv2d(x, weight, bias, stride, (0, 0), dilation, groups) + + +class Conv2dSame(nn.Conv2d): + """ Tensorflow like 'SAME' convolution wrapper for 2D convolutions + """ + + # pylint: disable=unused-argument + def __init__(self, in_channels, out_channels, kernel_size, stride=1, + padding=0, dilation=1, groups=1, bias=True): + super(Conv2dSame, self).__init__( + in_channels, out_channels, kernel_size, stride, 0, dilation, groups, bias) + + def forward(self, x): + return conv2d_same(x, self.weight, self.bias, self.stride, self.padding, self.dilation, self.groups) + + +class Conv2dSameExport(nn.Conv2d): + """ ONNX export friendly Tensorflow like 'SAME' convolution wrapper for 2D convolutions + + NOTE: This does not currently work with torch.jit.script + """ + + # pylint: disable=unused-argument + def __init__(self, in_channels, out_channels, kernel_size, stride=1, + padding=0, dilation=1, groups=1, bias=True): + super(Conv2dSameExport, self).__init__( + in_channels, out_channels, kernel_size, stride, 0, dilation, groups, bias) + self.pad = None + self.pad_input_size = (0, 0) + + def forward(self, x): + input_size = x.size()[-2:] + if self.pad is None: + pad_arg = _same_pad_arg(input_size, self.weight.size()[-2:], self.stride, self.dilation) + self.pad = nn.ZeroPad2d(pad_arg) + self.pad_input_size = input_size + + if self.pad is not None: + x = self.pad(x) + return F.conv2d( + x, self.weight, self.bias, self.stride, self.padding, self.dilation, self.groups) + + +def get_padding_value(padding, kernel_size, **kwargs): + dynamic = False + if isinstance(padding, str): + # for any string padding, the padding will be calculated for you, one of three ways + padding = padding.lower() + if padding == 'same': + # TF compatible 'SAME' padding, has a performance and GPU memory allocation impact + if _is_static_pad(kernel_size, **kwargs): + # static case, no extra overhead + padding = _get_padding(kernel_size, **kwargs) + else: + # dynamic padding + padding = 0 + dynamic = True + elif padding == 'valid': + # 'VALID' padding, same as padding=0 + padding = 0 + else: + # Default to PyTorch style 'same'-ish symmetric padding + padding = _get_padding(kernel_size, **kwargs) + return padding, dynamic + + +def create_conv2d_pad(in_chs, out_chs, kernel_size, **kwargs): + padding = kwargs.pop('padding', '') + kwargs.setdefault('bias', False) + padding, is_dynamic = get_padding_value(padding, kernel_size, **kwargs) + if is_dynamic: + if is_exportable(): + assert not is_scriptable() + return Conv2dSameExport(in_chs, out_chs, kernel_size, **kwargs) + else: + return Conv2dSame(in_chs, out_chs, kernel_size, **kwargs) + else: + return nn.Conv2d(in_chs, out_chs, kernel_size, padding=padding, **kwargs) + + +class MixedConv2d(nn.ModuleDict): + """ Mixed Grouped Convolution + Based on MDConv and GroupedConv in MixNet impl: + https://github.com/tensorflow/tpu/blob/master/models/official/mnasnet/mixnet/custom_layers.py + """ + + def __init__(self, in_channels, out_channels, kernel_size=3, + stride=1, padding='', dilation=1, depthwise=False, **kwargs): + super(MixedConv2d, self).__init__() + + kernel_size = kernel_size if isinstance(kernel_size, list) else [kernel_size] + num_groups = len(kernel_size) + in_splits = _split_channels(in_channels, num_groups) + out_splits = _split_channels(out_channels, num_groups) + self.in_channels = sum(in_splits) + self.out_channels = sum(out_splits) + for idx, (k, in_ch, out_ch) in enumerate(zip(kernel_size, in_splits, out_splits)): + conv_groups = out_ch if depthwise else 1 + self.add_module( + str(idx), + create_conv2d_pad( + in_ch, out_ch, k, stride=stride, + padding=padding, dilation=dilation, groups=conv_groups, **kwargs) + ) + self.splits = in_splits + + def forward(self, x): + x_split = torch.split(x, self.splits, 1) + x_out = [conv(x_split[i]) for i, conv in enumerate(self.values())] + x = torch.cat(x_out, 1) + return x + + +def get_condconv_initializer(initializer, num_experts, expert_shape): + def condconv_initializer(weight): + """CondConv initializer function.""" + num_params = np.prod(expert_shape) + if (len(weight.shape) != 2 or weight.shape[0] != num_experts or + weight.shape[1] != num_params): + raise (ValueError( + 'CondConv variables must have shape [num_experts, num_params]')) + for i in range(num_experts): + initializer(weight[i].view(expert_shape)) + return condconv_initializer + + +class CondConv2d(nn.Module): + """ Conditional Convolution + Inspired by: https://github.com/tensorflow/tpu/blob/master/models/official/efficientnet/condconv/condconv_layers.py + + Grouped convolution hackery for parallel execution of the per-sample kernel filters inspired by this discussion: + https://github.com/pytorch/pytorch/issues/17983 + """ + __constants__ = ['bias', 'in_channels', 'out_channels', 'dynamic_padding'] + + def __init__(self, in_channels, out_channels, kernel_size=3, + stride=1, padding='', dilation=1, groups=1, bias=False, num_experts=4): + super(CondConv2d, self).__init__() + + self.in_channels = in_channels + self.out_channels = out_channels + self.kernel_size = _pair(kernel_size) + self.stride = _pair(stride) + padding_val, is_padding_dynamic = get_padding_value( + padding, kernel_size, stride=stride, dilation=dilation) + self.dynamic_padding = is_padding_dynamic # if in forward to work with torchscript + self.padding = _pair(padding_val) + self.dilation = _pair(dilation) + self.groups = groups + self.num_experts = num_experts + + self.weight_shape = (self.out_channels, self.in_channels // self.groups) + self.kernel_size + weight_num_param = 1 + for wd in self.weight_shape: + weight_num_param *= wd + self.weight = torch.nn.Parameter(torch.Tensor(self.num_experts, weight_num_param)) + + if bias: + self.bias_shape = (self.out_channels,) + self.bias = torch.nn.Parameter(torch.Tensor(self.num_experts, self.out_channels)) + else: + self.register_parameter('bias', None) + + self.reset_parameters() + + def reset_parameters(self): + init_weight = get_condconv_initializer( + partial(nn.init.kaiming_uniform_, a=math.sqrt(5)), self.num_experts, self.weight_shape) + init_weight(self.weight) + if self.bias is not None: + fan_in = np.prod(self.weight_shape[1:]) + bound = 1 / math.sqrt(fan_in) + init_bias = get_condconv_initializer( + partial(nn.init.uniform_, a=-bound, b=bound), self.num_experts, self.bias_shape) + init_bias(self.bias) + + def forward(self, x, routing_weights): + B, C, H, W = x.shape + weight = torch.matmul(routing_weights, self.weight) + new_weight_shape = (B * self.out_channels, self.in_channels // self.groups) + self.kernel_size + weight = weight.view(new_weight_shape) + bias = None + if self.bias is not None: + bias = torch.matmul(routing_weights, self.bias) + bias = bias.view(B * self.out_channels) + # move batch elements with channels so each batch element can be efficiently convolved with separate kernel + x = x.view(1, B * C, H, W) + if self.dynamic_padding: + out = conv2d_same( + x, weight, bias, stride=self.stride, padding=self.padding, + dilation=self.dilation, groups=self.groups * B) + else: + out = F.conv2d( + x, weight, bias, stride=self.stride, padding=self.padding, + dilation=self.dilation, groups=self.groups * B) + out = out.permute([1, 0, 2, 3]).view(B, self.out_channels, out.shape[-2], out.shape[-1]) + + # Literal port (from TF definition) + # x = torch.split(x, 1, 0) + # weight = torch.split(weight, 1, 0) + # if self.bias is not None: + # bias = torch.matmul(routing_weights, self.bias) + # bias = torch.split(bias, 1, 0) + # else: + # bias = [None] * B + # out = [] + # for xi, wi, bi in zip(x, weight, bias): + # wi = wi.view(*self.weight_shape) + # if bi is not None: + # bi = bi.view(*self.bias_shape) + # out.append(self.conv_fn( + # xi, wi, bi, stride=self.stride, padding=self.padding, + # dilation=self.dilation, groups=self.groups)) + # out = torch.cat(out, 0) + return out + + +def select_conv2d(in_chs, out_chs, kernel_size, **kwargs): + assert 'groups' not in kwargs # only use 'depthwise' bool arg + if isinstance(kernel_size, list): + assert 'num_experts' not in kwargs # MixNet + CondConv combo not supported currently + # We're going to use only lists for defining the MixedConv2d kernel groups, + # ints, tuples, other iterables will continue to pass to normal conv and specify h, w. + m = MixedConv2d(in_chs, out_chs, kernel_size, **kwargs) + else: + depthwise = kwargs.pop('depthwise', False) + groups = out_chs if depthwise else 1 + if 'num_experts' in kwargs and kwargs['num_experts'] > 0: + m = CondConv2d(in_chs, out_chs, kernel_size, groups=groups, **kwargs) + else: + m = create_conv2d_pad(in_chs, out_chs, kernel_size, groups=groups, **kwargs) + return m diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/efficientnet_builder.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/efficientnet_builder.py new file mode 100644 index 00000000000..4da05853890 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/efficientnet_builder.py @@ -0,0 +1,683 @@ +""" EfficientNet / MobileNetV3 Blocks and Builder + +Copyright 2020 Ross Wightman +""" +import re +from copy import deepcopy + +from .conv2d_layers import CondConv2d, get_condconv_initializer, math, partial, select_conv2d +from geffnet.activations import F, get_act_layer, nn, sigmoid, torch + +__all__ = ['get_bn_args_tf', 'resolve_bn_args', 'resolve_se_args', 'resolve_act_layer', 'make_divisible', + 'round_channels', 'drop_connect', 'SqueezeExcite', 'ConvBnAct', 'DepthwiseSeparableConv', + 'InvertedResidual', 'CondConvResidual', 'EdgeResidual', 'EfficientNetBuilder', 'decode_arch_def', + 'initialize_weight_default', 'initialize_weight_goog', 'BN_MOMENTUM_TF_DEFAULT', 'BN_EPS_TF_DEFAULT' +] + +# Defaults used for Google/Tensorflow training of mobile networks /w RMSprop as per +# papers and TF reference implementations. PT momentum equiv for TF decay is (1 - TF decay) +# NOTE: momentum varies btw .99 and .9997 depending on source +# .99 in official TF TPU impl +# .9997 (/w .999 in search space) for paper +# +# PyTorch defaults are momentum = .1, eps = 1e-5 +# +BN_MOMENTUM_TF_DEFAULT = 1 - 0.99 +BN_EPS_TF_DEFAULT = 1e-3 +_BN_ARGS_TF = dict(momentum=BN_MOMENTUM_TF_DEFAULT, eps=BN_EPS_TF_DEFAULT) + + +def get_bn_args_tf(): + return _BN_ARGS_TF.copy() + + +def resolve_bn_args(kwargs): + bn_args = get_bn_args_tf() if kwargs.pop('bn_tf', False) else {} + bn_momentum = kwargs.pop('bn_momentum', None) + if bn_momentum is not None: + bn_args['momentum'] = bn_momentum + bn_eps = kwargs.pop('bn_eps', None) + if bn_eps is not None: + bn_args['eps'] = bn_eps + return bn_args + + +_SE_ARGS_DEFAULT = dict( + gate_fn=sigmoid, + act_layer=None, # None == use containing block's activation layer + reduce_mid=False, + divisor=1) + + +def resolve_se_args(kwargs, in_chs, act_layer=None): + se_kwargs = kwargs.copy() if kwargs is not None else {} + # fill in args that aren't specified with the defaults + for k, v in _SE_ARGS_DEFAULT.items(): + se_kwargs.setdefault(k, v) + # some models, like MobilNetV3, calculate SE reduction chs from the containing block's mid_ch instead of in_ch + if not se_kwargs.pop('reduce_mid'): + se_kwargs['reduced_base_chs'] = in_chs + # act_layer override, if it remains None, the containing block's act_layer will be used + if se_kwargs['act_layer'] is None: + assert act_layer is not None + se_kwargs['act_layer'] = act_layer + return se_kwargs + + +def resolve_act_layer(kwargs, default='relu'): + act_layer = kwargs.pop('act_layer', default) + if isinstance(act_layer, str): + act_layer = get_act_layer(act_layer) + return act_layer + + +def make_divisible(v: int, divisor: int = 8, min_value: int = None): + min_value = min_value or divisor + new_v = max(min_value, int(v + divisor / 2) // divisor * divisor) + if new_v < 0.9 * v: # ensure round down does not go down by more than 10%. + new_v += divisor + return new_v + + +def round_channels(channels, multiplier=1.0, divisor=8, channel_min=None): + """Round number of filters based on depth multiplier.""" + if not multiplier: + return channels + channels *= multiplier + return make_divisible(channels, divisor, channel_min) + + +def drop_connect(inputs, training: bool = False, drop_connect_rate: float = 0.): + """Apply drop connect.""" + if not training: + return inputs + + keep_prob = 1 - drop_connect_rate + random_tensor = keep_prob + torch.rand( + (inputs.size()[0], 1, 1, 1), dtype=inputs.dtype, device=inputs.device) + random_tensor.floor_() # binarize + output = inputs.div(keep_prob) * random_tensor + return output + + +class SqueezeExcite(nn.Module): + + def __init__(self, in_chs, se_ratio=0.25, reduced_base_chs=None, act_layer=nn.ReLU, gate_fn=sigmoid, divisor=1): + super(SqueezeExcite, self).__init__() + reduced_chs = make_divisible((reduced_base_chs or in_chs) * se_ratio, divisor) + self.conv_reduce = nn.Conv2d(in_chs, reduced_chs, 1, bias=True) + self.act1 = act_layer(inplace=True) + self.conv_expand = nn.Conv2d(reduced_chs, in_chs, 1, bias=True) + self.gate_fn = gate_fn + + def forward(self, x): + x_se = x.mean((2, 3), keepdim=True) + x_se = self.conv_reduce(x_se) + x_se = self.act1(x_se) + x_se = self.conv_expand(x_se) + x = x * self.gate_fn(x_se) + return x + + +class ConvBnAct(nn.Module): + def __init__(self, in_chs, out_chs, kernel_size, + stride=1, pad_type='', act_layer=nn.ReLU, norm_layer=nn.BatchNorm2d, norm_kwargs=None): + super(ConvBnAct, self).__init__() + assert stride in [1, 2] + norm_kwargs = norm_kwargs or {} + self.conv = select_conv2d(in_chs, out_chs, kernel_size, stride=stride, padding=pad_type) + self.bn1 = norm_layer(out_chs, **norm_kwargs) + self.act1 = act_layer(inplace=True) + + def forward(self, x): + x = self.conv(x) + x = self.bn1(x) + x = self.act1(x) + return x + + +class DepthwiseSeparableConv(nn.Module): + """ DepthwiseSeparable block + Used for DS convs in MobileNet-V1 and in the place of IR blocks with an expansion + factor of 1.0. This is an alternative to having a IR with optional first pw conv. + """ + def __init__(self, in_chs, out_chs, dw_kernel_size=3, + stride=1, pad_type='', act_layer=nn.ReLU, noskip=False, + pw_kernel_size=1, pw_act=False, se_ratio=0., se_kwargs=None, + norm_layer=nn.BatchNorm2d, norm_kwargs=None, drop_connect_rate=0.): + super(DepthwiseSeparableConv, self).__init__() + assert stride in [1, 2] + norm_kwargs = norm_kwargs or {} + self.has_residual = (stride == 1 and in_chs == out_chs) and not noskip + self.drop_connect_rate = drop_connect_rate + + self.conv_dw = select_conv2d( + in_chs, in_chs, dw_kernel_size, stride=stride, padding=pad_type, depthwise=True) + self.bn1 = norm_layer(in_chs, **norm_kwargs) + self.act1 = act_layer(inplace=True) + + # Squeeze-and-excitation + if se_ratio is not None and se_ratio > 0.: + se_kwargs = resolve_se_args(se_kwargs, in_chs, act_layer) + self.se = SqueezeExcite(in_chs, se_ratio=se_ratio, **se_kwargs) + else: + self.se = nn.Identity() + + self.conv_pw = select_conv2d(in_chs, out_chs, pw_kernel_size, padding=pad_type) + self.bn2 = norm_layer(out_chs, **norm_kwargs) + self.act2 = act_layer(inplace=True) if pw_act else nn.Identity() + + def forward(self, x): + residual = x + + x = self.conv_dw(x) + x = self.bn1(x) + x = self.act1(x) + + x = self.se(x) + + x = self.conv_pw(x) + x = self.bn2(x) + x = self.act2(x) + + if self.has_residual: + if self.drop_connect_rate > 0.: + x = drop_connect(x, self.training, self.drop_connect_rate) + x += residual + return x + + +class InvertedResidual(nn.Module): + """ Inverted residual block w/ optional SE""" + + def __init__(self, in_chs, out_chs, dw_kernel_size=3, + stride=1, pad_type='', act_layer=nn.ReLU, noskip=False, + exp_ratio=1.0, exp_kernel_size=1, pw_kernel_size=1, + se_ratio=0., se_kwargs=None, norm_layer=nn.BatchNorm2d, norm_kwargs=None, + conv_kwargs=None, drop_connect_rate=0.): + super(InvertedResidual, self).__init__() + norm_kwargs = norm_kwargs or {} + conv_kwargs = conv_kwargs or {} + mid_chs: int = make_divisible(in_chs * exp_ratio) + self.has_residual = (in_chs == out_chs and stride == 1) and not noskip + self.drop_connect_rate = drop_connect_rate + + # Point-wise expansion + self.conv_pw = select_conv2d(in_chs, mid_chs, exp_kernel_size, padding=pad_type, **conv_kwargs) + self.bn1 = norm_layer(mid_chs, **norm_kwargs) + self.act1 = act_layer(inplace=True) + + # Depth-wise convolution + self.conv_dw = select_conv2d( + mid_chs, mid_chs, dw_kernel_size, stride=stride, padding=pad_type, depthwise=True, **conv_kwargs) + self.bn2 = norm_layer(mid_chs, **norm_kwargs) + self.act2 = act_layer(inplace=True) + + # Squeeze-and-excitation + if se_ratio is not None and se_ratio > 0.: + se_kwargs = resolve_se_args(se_kwargs, in_chs, act_layer) + self.se = SqueezeExcite(mid_chs, se_ratio=se_ratio, **se_kwargs) + else: + self.se = nn.Identity() # for jit.script compat + + # Point-wise linear projection + self.conv_pwl = select_conv2d(mid_chs, out_chs, pw_kernel_size, padding=pad_type, **conv_kwargs) + self.bn3 = norm_layer(out_chs, **norm_kwargs) + + def forward(self, x): + residual = x + + # Point-wise expansion + x = self.conv_pw(x) + x = self.bn1(x) + x = self.act1(x) + + # Depth-wise convolution + x = self.conv_dw(x) + x = self.bn2(x) + x = self.act2(x) + + # Squeeze-and-excitation + x = self.se(x) + + # Point-wise linear projection + x = self.conv_pwl(x) + x = self.bn3(x) + + if self.has_residual: + if self.drop_connect_rate > 0.: + x = drop_connect(x, self.training, self.drop_connect_rate) + x += residual + return x + + +class CondConvResidual(InvertedResidual): + """ Inverted residual block w/ CondConv routing""" + + def __init__(self, in_chs, out_chs, dw_kernel_size=3, + stride=1, pad_type='', act_layer=nn.ReLU, noskip=False, + exp_ratio=1.0, exp_kernel_size=1, pw_kernel_size=1, + se_ratio=0., se_kwargs=None, norm_layer=nn.BatchNorm2d, norm_kwargs=None, + num_experts=0, drop_connect_rate=0.): + + self.num_experts = num_experts + conv_kwargs = dict(num_experts=self.num_experts) + + super(CondConvResidual, self).__init__( + in_chs, out_chs, dw_kernel_size=dw_kernel_size, stride=stride, pad_type=pad_type, + act_layer=act_layer, noskip=noskip, exp_ratio=exp_ratio, exp_kernel_size=exp_kernel_size, + pw_kernel_size=pw_kernel_size, se_ratio=se_ratio, se_kwargs=se_kwargs, + norm_layer=norm_layer, norm_kwargs=norm_kwargs, conv_kwargs=conv_kwargs, + drop_connect_rate=drop_connect_rate) + + self.routing_fn = nn.Linear(in_chs, self.num_experts) + + def forward(self, x): + residual = x + + # CondConv routing + pooled_inputs = F.adaptive_avg_pool2d(x, 1).flatten(1) + routing_weights = torch.sigmoid(self.routing_fn(pooled_inputs)) + + # Point-wise expansion + x = self.conv_pw(x, routing_weights) + x = self.bn1(x) + x = self.act1(x) + + # Depth-wise convolution + x = self.conv_dw(x, routing_weights) + x = self.bn2(x) + x = self.act2(x) + + # Squeeze-and-excitation + x = self.se(x) + + # Point-wise linear projection + x = self.conv_pwl(x, routing_weights) + x = self.bn3(x) + + if self.has_residual: + if self.drop_connect_rate > 0.: + x = drop_connect(x, self.training, self.drop_connect_rate) + x += residual + return x + + +class EdgeResidual(nn.Module): + """ EdgeTPU Residual block with expansion convolution followed by pointwise-linear w/ stride""" + + def __init__(self, in_chs, out_chs, exp_kernel_size=3, exp_ratio=1.0, fake_in_chs=0, + stride=1, pad_type='', act_layer=nn.ReLU, noskip=False, pw_kernel_size=1, + se_ratio=0., se_kwargs=None, norm_layer=nn.BatchNorm2d, norm_kwargs=None, drop_connect_rate=0.): + super(EdgeResidual, self).__init__() + norm_kwargs = norm_kwargs or {} + mid_chs = make_divisible(fake_in_chs * exp_ratio) if fake_in_chs > 0 else make_divisible(in_chs * exp_ratio) + self.has_residual = (in_chs == out_chs and stride == 1) and not noskip + self.drop_connect_rate = drop_connect_rate + + # Expansion convolution + self.conv_exp = select_conv2d(in_chs, mid_chs, exp_kernel_size, padding=pad_type) + self.bn1 = norm_layer(mid_chs, **norm_kwargs) + self.act1 = act_layer(inplace=True) + + # Squeeze-and-excitation + if se_ratio is not None and se_ratio > 0.: + se_kwargs = resolve_se_args(se_kwargs, in_chs, act_layer) + self.se = SqueezeExcite(mid_chs, se_ratio=se_ratio, **se_kwargs) + else: + self.se = nn.Identity() + + # Point-wise linear projection + self.conv_pwl = select_conv2d(mid_chs, out_chs, pw_kernel_size, stride=stride, padding=pad_type) + self.bn2 = nn.BatchNorm2d(out_chs, **norm_kwargs) + + def forward(self, x): + residual = x + + # Expansion convolution + x = self.conv_exp(x) + x = self.bn1(x) + x = self.act1(x) + + # Squeeze-and-excitation + x = self.se(x) + + # Point-wise linear projection + x = self.conv_pwl(x) + x = self.bn2(x) + + if self.has_residual: + if self.drop_connect_rate > 0.: + x = drop_connect(x, self.training, self.drop_connect_rate) + x += residual + + return x + + +class EfficientNetBuilder: + """ Build Trunk Blocks for Efficient/Mobile Networks + + This ended up being somewhat of a cross between + https://github.com/tensorflow/tpu/blob/master/models/official/mnasnet/mnasnet_models.py + and + https://github.com/facebookresearch/maskrcnn-benchmark/blob/master/maskrcnn_benchmark/modeling/backbone/fbnet_builder.py + + """ + + def __init__(self, channel_multiplier=1.0, channel_divisor=8, channel_min=None, + pad_type='', act_layer=None, se_kwargs=None, + norm_layer=nn.BatchNorm2d, norm_kwargs=None, drop_connect_rate=0.): + self.channel_multiplier = channel_multiplier + self.channel_divisor = channel_divisor + self.channel_min = channel_min + self.pad_type = pad_type + self.act_layer = act_layer + self.se_kwargs = se_kwargs + self.norm_layer = norm_layer + self.norm_kwargs = norm_kwargs + self.drop_connect_rate = drop_connect_rate + + # updated during build + self.in_chs = None + self.block_idx = 0 + self.block_count = 0 + + def _round_channels(self, chs): + return round_channels(chs, self.channel_multiplier, self.channel_divisor, self.channel_min) + + def _make_block(self, ba): + bt = ba.pop('block_type') + ba['in_chs'] = self.in_chs + ba['out_chs'] = self._round_channels(ba['out_chs']) + if 'fake_in_chs' in ba and ba['fake_in_chs']: + # FIXME this is a hack to work around mismatch in origin impl input filters for EdgeTPU + ba['fake_in_chs'] = self._round_channels(ba['fake_in_chs']) + ba['norm_layer'] = self.norm_layer + ba['norm_kwargs'] = self.norm_kwargs + ba['pad_type'] = self.pad_type + # block act fn overrides the model default + ba['act_layer'] = ba['act_layer'] if ba['act_layer'] is not None else self.act_layer + assert ba['act_layer'] is not None + if bt == 'ir': + ba['drop_connect_rate'] = self.drop_connect_rate * self.block_idx / self.block_count + ba['se_kwargs'] = self.se_kwargs + if ba.get('num_experts', 0) > 0: + block = CondConvResidual(**ba) + else: + block = InvertedResidual(**ba) + elif bt == 'ds' or bt == 'dsa': + ba['drop_connect_rate'] = self.drop_connect_rate * self.block_idx / self.block_count + ba['se_kwargs'] = self.se_kwargs + block = DepthwiseSeparableConv(**ba) + elif bt == 'er': + ba['drop_connect_rate'] = self.drop_connect_rate * self.block_idx / self.block_count + ba['se_kwargs'] = self.se_kwargs + block = EdgeResidual(**ba) + elif bt == 'cn': + block = ConvBnAct(**ba) + else: + assert False, 'Uknkown block type (%s) while building model.' % bt + self.in_chs = ba['out_chs'] # update in_chs for arg of next block + return block + + def _make_stack(self, stack_args): + blocks = [] + # each stack (stage) contains a list of block arguments + for i, ba in enumerate(stack_args): + if i >= 1: + # only the first block in any stack can have a stride > 1 + ba['stride'] = 1 + block = self._make_block(ba) + blocks.append(block) + self.block_idx += 1 # incr global idx (across all stacks) + return nn.Sequential(*blocks) + + def __call__(self, in_chs, block_args): + """ Build the blocks + Args: + in_chs: Number of input-channels passed to first block + block_args: A list of lists, outer list defines stages, inner + list contains strings defining block configuration(s) + Return: + List of block stacks (each stack wrapped in nn.Sequential) + """ + self.in_chs = in_chs + self.block_count = sum([len(x) for x in block_args]) + self.block_idx = 0 + blocks = [] + # outer list of block_args defines the stacks ('stages' by some conventions) + for stack_idx, stack in enumerate(block_args): + assert isinstance(stack, list) + stack = self._make_stack(stack) + blocks.append(stack) + return blocks + + +def _parse_ksize(ss): + if ss.isdigit(): + return int(ss) + else: + return [int(k) for k in ss.split('.')] + + +def _decode_block_str(block_str): + """ Decode block definition string + + Gets a list of block arg (dicts) through a string notation of arguments. + E.g. ir_r2_k3_s2_e1_i32_o16_se0.25_noskip + + All args can exist in any order with the exception of the leading string which + is assumed to indicate the block type. + + leading string - block type ( + ir = InvertedResidual, ds = DepthwiseSep, dsa = DeptwhiseSep with pw act, cn = ConvBnAct) + r - number of repeat blocks, + k - kernel size, + s - strides (1-9), + e - expansion ratio, + c - output channels, + se - squeeze/excitation ratio + n - activation fn ('re', 'r6', 'hs', or 'sw') + Args: + block_str: a string representation of block arguments. + Returns: + A list of block args (dicts) + Raises: + ValueError: if the string def not properly specified (TODO) + """ + assert isinstance(block_str, str) + ops = block_str.split('_') + block_type = ops[0] # take the block type off the front + ops = ops[1:] + options = {} + noskip = False + for op in ops: + # string options being checked on individual basis, combine if they grow + if op == 'noskip': + noskip = True + elif op.startswith('n'): + # activation fn + key = op[0] + v = op[1:] + if v == 're': + value = get_act_layer('relu') + elif v == 'r6': + value = get_act_layer('relu6') + elif v == 'hs': + value = get_act_layer('hard_swish') + elif v == 'sw': + value = get_act_layer('swish') + else: + continue + options[key] = value + else: + # all numeric options + splits = re.split(r'(\d.*)', op) + if len(splits) >= 2: + key, value = splits[:2] + options[key] = value + + # if act_layer is None, the model default (passed to model init) will be used + act_layer = options['n'] if 'n' in options else None + exp_kernel_size = _parse_ksize(options['a']) if 'a' in options else 1 + pw_kernel_size = _parse_ksize(options['p']) if 'p' in options else 1 + fake_in_chs = int(options['fc']) if 'fc' in options else 0 # FIXME hack to deal with in_chs issue in TPU def + + num_repeat = int(options['r']) + # each type of block has different valid arguments, fill accordingly + if block_type == 'ir': + block_args = dict( + block_type=block_type, + dw_kernel_size=_parse_ksize(options['k']), + exp_kernel_size=exp_kernel_size, + pw_kernel_size=pw_kernel_size, + out_chs=int(options['c']), + exp_ratio=float(options['e']), + se_ratio=float(options['se']) if 'se' in options else None, + stride=int(options['s']), + act_layer=act_layer, + noskip=noskip, + ) + if 'cc' in options: + block_args['num_experts'] = int(options['cc']) + elif block_type == 'ds' or block_type == 'dsa': + block_args = dict( + block_type=block_type, + dw_kernel_size=_parse_ksize(options['k']), + pw_kernel_size=pw_kernel_size, + out_chs=int(options['c']), + se_ratio=float(options['se']) if 'se' in options else None, + stride=int(options['s']), + act_layer=act_layer, + pw_act=block_type == 'dsa', + noskip=block_type == 'dsa' or noskip, + ) + elif block_type == 'er': + block_args = dict( + block_type=block_type, + exp_kernel_size=_parse_ksize(options['k']), + pw_kernel_size=pw_kernel_size, + out_chs=int(options['c']), + exp_ratio=float(options['e']), + fake_in_chs=fake_in_chs, + se_ratio=float(options['se']) if 'se' in options else None, + stride=int(options['s']), + act_layer=act_layer, + noskip=noskip, + ) + elif block_type == 'cn': + block_args = dict( + block_type=block_type, + kernel_size=int(options['k']), + out_chs=int(options['c']), + stride=int(options['s']), + act_layer=act_layer, + ) + else: + assert False, 'Unknown block type (%s)' % block_type + + return block_args, num_repeat + + +def _scale_stage_depth(stack_args, repeats, depth_multiplier=1.0, depth_trunc='ceil'): + """ Per-stage depth scaling + Scales the block repeats in each stage. This depth scaling impl maintains + compatibility with the EfficientNet scaling method, while allowing sensible + scaling for other models that may have multiple block arg definitions in each stage. + """ + + # We scale the total repeat count for each stage, there may be multiple + # block arg defs per stage so we need to sum. + num_repeat = sum(repeats) + if depth_trunc == 'round': + # Truncating to int by rounding allows stages with few repeats to remain + # proportionally smaller for longer. This is a good choice when stage definitions + # include single repeat stages that we'd prefer to keep that way as long as possible + num_repeat_scaled = max(1, round(num_repeat * depth_multiplier)) + else: + # The default for EfficientNet truncates repeats to int via 'ceil'. + # Any multiplier > 1.0 will result in an increased depth for every stage. + num_repeat_scaled = int(math.ceil(num_repeat * depth_multiplier)) + + # Proportionally distribute repeat count scaling to each block definition in the stage. + # Allocation is done in reverse as it results in the first block being less likely to be scaled. + # The first block makes less sense to repeat in most of the arch definitions. + repeats_scaled = [] + for r in repeats[::-1]: + rs = max(1, round((r / num_repeat * num_repeat_scaled))) + repeats_scaled.append(rs) + num_repeat -= r + num_repeat_scaled -= rs + repeats_scaled = repeats_scaled[::-1] + + # Apply the calculated scaling to each block arg in the stage + sa_scaled = [] + for ba, rep in zip(stack_args, repeats_scaled): + sa_scaled.extend([deepcopy(ba) for _ in range(rep)]) + return sa_scaled + + +def decode_arch_def(arch_def, depth_multiplier=1.0, depth_trunc='ceil', experts_multiplier=1, fix_first_last=False): + arch_args = [] + for stack_idx, block_strings in enumerate(arch_def): + assert isinstance(block_strings, list) + stack_args = [] + repeats = [] + for block_str in block_strings: + assert isinstance(block_str, str) + ba, rep = _decode_block_str(block_str) + if ba.get('num_experts', 0) > 0 and experts_multiplier > 1: + ba['num_experts'] *= experts_multiplier + stack_args.append(ba) + repeats.append(rep) + if fix_first_last and (stack_idx == 0 or stack_idx == len(arch_def) - 1): + arch_args.append(_scale_stage_depth(stack_args, repeats, 1.0, depth_trunc)) + else: + arch_args.append(_scale_stage_depth(stack_args, repeats, depth_multiplier, depth_trunc)) + return arch_args + + +def initialize_weight_goog(m, n='', fix_group_fanout=True): + # weight init as per Tensorflow Official impl + # https://github.com/tensorflow/tpu/blob/master/models/official/mnasnet/mnasnet_model.py + if isinstance(m, CondConv2d): + fan_out = m.kernel_size[0] * m.kernel_size[1] * m.out_channels + if fix_group_fanout: + fan_out //= m.groups + init_weight_fn = get_condconv_initializer( + lambda w: w.data.normal_(0, math.sqrt(2.0 / fan_out)), m.num_experts, m.weight_shape) + init_weight_fn(m.weight) + if m.bias is not None: + m.bias.data.zero_() + elif isinstance(m, nn.Conv2d): + fan_out = m.kernel_size[0] * m.kernel_size[1] * m.out_channels + if fix_group_fanout: + fan_out //= m.groups + m.weight.data.normal_(0, math.sqrt(2.0 / fan_out)) + if m.bias is not None: + m.bias.data.zero_() + elif isinstance(m, nn.BatchNorm2d): + m.weight.data.fill_(1.0) + m.bias.data.zero_() + elif isinstance(m, nn.Linear): + fan_out = m.weight.size(0) # fan-out + fan_in = 0 + if 'routing_fn' in n: + fan_in = m.weight.size(1) + init_range = 1.0 / math.sqrt(fan_in + fan_out) + m.weight.data.uniform_(-init_range, init_range) + m.bias.data.zero_() + + +def initialize_weight_default(m, n=''): + if isinstance(m, CondConv2d): + init_fn = get_condconv_initializer(partial( + nn.init.kaiming_normal_, mode='fan_out', nonlinearity='relu'), m.num_experts, m.weight_shape) + init_fn(m.weight) + elif isinstance(m, nn.Conv2d): + nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') + elif isinstance(m, nn.BatchNorm2d): + m.weight.data.fill_(1.0) + m.bias.data.zero_() + elif isinstance(m, nn.Linear): + nn.init.kaiming_uniform_(m.weight, mode='fan_in', nonlinearity='linear') diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/gen_efficientnet.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/gen_efficientnet.py new file mode 100644 index 00000000000..029799305ae --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/gen_efficientnet.py @@ -0,0 +1,1452 @@ +""" Generic Efficient Networks + +A generic MobileNet class with building blocks to support a variety of models: + +* EfficientNet (B0-B8, L2 + Tensorflow pretrained AutoAug/RandAug/AdvProp/NoisyStudent ports) + - EfficientNet: Rethinking Model Scaling for CNNs - https://arxiv.org/abs/1905.11946 + - CondConv: Conditionally Parameterized Convolutions for Efficient Inference - https://arxiv.org/abs/1904.04971 + - Adversarial Examples Improve Image Recognition - https://arxiv.org/abs/1911.09665 + - Self-training with Noisy Student improves ImageNet classification - https://arxiv.org/abs/1911.04252 + +* EfficientNet-Lite + +* MixNet (Small, Medium, and Large) + - MixConv: Mixed Depthwise Convolutional Kernels - https://arxiv.org/abs/1907.09595 + +* MNasNet B1, A1 (SE), Small + - MnasNet: Platform-Aware Neural Architecture Search for Mobile - https://arxiv.org/abs/1807.11626 + +* FBNet-C + - FBNet: Hardware-Aware Efficient ConvNet Design via Differentiable NAS - https://arxiv.org/abs/1812.03443 + +* Single-Path NAS Pixel1 + - Single-Path NAS: Designing Hardware-Efficient ConvNets - https://arxiv.org/abs/1904.02877 + +* And likely more... + +Hacked together by / Copyright 2020 Ross Wightman +""" +import torch.nn as nn +import torch.nn.functional as F + +from .config import layer_config_kwargs, is_scriptable +from .conv2d_layers import select_conv2d +from .helpers import load_pretrained +from .efficientnet_builder import (BN_EPS_TF_DEFAULT, EfficientNetBuilder, decode_arch_def, + initialize_weight_default, initialize_weight_goog, + resolve_act_layer, resolve_bn_args, round_channels) + +__all__ = ['GenEfficientNet', 'mnasnet_050', 'mnasnet_075', 'mnasnet_100', 'mnasnet_b1', 'mnasnet_140', + 'semnasnet_050', 'semnasnet_075', 'semnasnet_100', 'mnasnet_a1', 'semnasnet_140', 'mnasnet_small', + 'mobilenetv2_100', 'mobilenetv2_140', 'mobilenetv2_110d', 'mobilenetv2_120d', + 'fbnetc_100', 'spnasnet_100', 'efficientnet_b0', 'efficientnet_b1', 'efficientnet_b2', 'efficientnet_b3', + 'efficientnet_b4', 'efficientnet_b5', 'efficientnet_b6', 'efficientnet_b7', 'efficientnet_b8', + 'efficientnet_l2', 'efficientnet_es', 'efficientnet_em', 'efficientnet_el', + 'efficientnet_cc_b0_4e', 'efficientnet_cc_b0_8e', 'efficientnet_cc_b1_8e', + 'efficientnet_lite0', 'efficientnet_lite1', 'efficientnet_lite2', 'efficientnet_lite3', 'efficientnet_lite4', + 'tf_efficientnet_b0', 'tf_efficientnet_b1', 'tf_efficientnet_b2', 'tf_efficientnet_b3', + 'tf_efficientnet_b4', 'tf_efficientnet_b5', 'tf_efficientnet_b6', 'tf_efficientnet_b7', 'tf_efficientnet_b8', + 'tf_efficientnet_b0_ap', 'tf_efficientnet_b1_ap', 'tf_efficientnet_b2_ap', 'tf_efficientnet_b3_ap', + 'tf_efficientnet_b4_ap', 'tf_efficientnet_b5_ap', 'tf_efficientnet_b6_ap', 'tf_efficientnet_b7_ap', + 'tf_efficientnet_b8_ap', 'tf_efficientnet_b0_ns', 'tf_efficientnet_b1_ns', 'tf_efficientnet_b2_ns', + 'tf_efficientnet_b3_ns', 'tf_efficientnet_b4_ns', 'tf_efficientnet_b5_ns', 'tf_efficientnet_b6_ns', + 'tf_efficientnet_b7_ns', 'tf_efficientnet_l2_ns', 'tf_efficientnet_l2_ns_475', + 'tf_efficientnet_es', 'tf_efficientnet_em', 'tf_efficientnet_el', + 'tf_efficientnet_cc_b0_4e', 'tf_efficientnet_cc_b0_8e', 'tf_efficientnet_cc_b1_8e', + 'tf_efficientnet_lite0', 'tf_efficientnet_lite1', 'tf_efficientnet_lite2', 'tf_efficientnet_lite3', + 'tf_efficientnet_lite4', + 'mixnet_s', 'mixnet_m', 'mixnet_l', 'mixnet_xl', 'tf_mixnet_s', 'tf_mixnet_m', 'tf_mixnet_l'] + + +model_urls = { + 'mnasnet_050': None, + 'mnasnet_075': None, + 'mnasnet_100': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/mnasnet_b1-74cb7081.pth', + 'mnasnet_140': None, + 'mnasnet_small': None, + + 'semnasnet_050': None, + 'semnasnet_075': None, + 'semnasnet_100': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/mnasnet_a1-d9418771.pth', + 'semnasnet_140': None, + + 'mobilenetv2_100': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/mobilenetv2_100_ra-b33bc2c4.pth', + 'mobilenetv2_110d': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/mobilenetv2_110d_ra-77090ade.pth', + 'mobilenetv2_120d': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/mobilenetv2_120d_ra-5987e2ed.pth', + 'mobilenetv2_140': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/mobilenetv2_140_ra-21a4e913.pth', + + 'fbnetc_100': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/fbnetc_100-c345b898.pth', + 'spnasnet_100': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/spnasnet_100-048bc3f4.pth', + + 'efficientnet_b0': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/efficientnet_b0_ra-3dd342df.pth', + 'efficientnet_b1': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/efficientnet_b1-533bc792.pth', + 'efficientnet_b2': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/efficientnet_b2_ra-bcdf34b7.pth', + 'efficientnet_b3': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/efficientnet_b3_ra2-cf984f9c.pth', + 'efficientnet_b4': None, + 'efficientnet_b5': None, + 'efficientnet_b6': None, + 'efficientnet_b7': None, + 'efficientnet_b8': None, + 'efficientnet_l2': None, + + 'efficientnet_es': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/efficientnet_es_ra-f111e99c.pth', + 'efficientnet_em': None, + 'efficientnet_el': None, + + 'efficientnet_cc_b0_4e': None, + 'efficientnet_cc_b0_8e': None, + 'efficientnet_cc_b1_8e': None, + + 'efficientnet_lite0': 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/efficientnet_lite0_ra-37913777.pth', + 'efficientnet_lite1': None, + 'efficientnet_lite2': None, + 'efficientnet_lite3': None, + 'efficientnet_lite4': None, + + 'tf_efficientnet_b0': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b0_aa-827b6e33.pth', + 'tf_efficientnet_b1': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b1_aa-ea7a6ee0.pth', + 'tf_efficientnet_b2': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b2_aa-60c94f97.pth', + 'tf_efficientnet_b3': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b3_aa-84b4657e.pth', + 'tf_efficientnet_b4': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b4_aa-818f208c.pth', + 'tf_efficientnet_b5': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b5_ra-9a3e5369.pth', + 'tf_efficientnet_b6': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b6_aa-80ba17e4.pth', + 'tf_efficientnet_b7': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b7_ra-6c08e654.pth', + 'tf_efficientnet_b8': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b8_ra-572d5dd9.pth', + + 'tf_efficientnet_b0_ap': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b0_ap-f262efe1.pth', + 'tf_efficientnet_b1_ap': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b1_ap-44ef0a3d.pth', + 'tf_efficientnet_b2_ap': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b2_ap-2f8e7636.pth', + 'tf_efficientnet_b3_ap': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b3_ap-aad25bdd.pth', + 'tf_efficientnet_b4_ap': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b4_ap-dedb23e6.pth', + 'tf_efficientnet_b5_ap': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b5_ap-9e82fae8.pth', + 'tf_efficientnet_b6_ap': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b6_ap-4ffb161f.pth', + 'tf_efficientnet_b7_ap': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b7_ap-ddb28fec.pth', + 'tf_efficientnet_b8_ap': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b8_ap-00e169fa.pth', + + 'tf_efficientnet_b0_ns': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b0_ns-c0e6a31c.pth', + 'tf_efficientnet_b1_ns': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b1_ns-99dd0c41.pth', + 'tf_efficientnet_b2_ns': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b2_ns-00306e48.pth', + 'tf_efficientnet_b3_ns': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b3_ns-9d44bf68.pth', + 'tf_efficientnet_b4_ns': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b4_ns-d6313a46.pth', + 'tf_efficientnet_b5_ns': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b5_ns-6f26d0cf.pth', + 'tf_efficientnet_b6_ns': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b6_ns-51548356.pth', + 'tf_efficientnet_b7_ns': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_b7_ns-1dbc32de.pth', + 'tf_efficientnet_l2_ns_475': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_l2_ns_475-bebbd00a.pth', + 'tf_efficientnet_l2_ns': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_l2_ns-df73bb44.pth', + + 'tf_efficientnet_es': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_es-ca1afbfe.pth', + 'tf_efficientnet_em': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_em-e78cfe58.pth', + 'tf_efficientnet_el': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_el-5143854e.pth', + + 'tf_efficientnet_cc_b0_4e': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_cc_b0_4e-4362b6b2.pth', + 'tf_efficientnet_cc_b0_8e': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_cc_b0_8e-66184a25.pth', + 'tf_efficientnet_cc_b1_8e': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_cc_b1_8e-f7c79ae1.pth', + + 'tf_efficientnet_lite0': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_lite0-0aa007d2.pth', + 'tf_efficientnet_lite1': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_lite1-bde8b488.pth', + 'tf_efficientnet_lite2': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_lite2-dcccb7df.pth', + 'tf_efficientnet_lite3': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_lite3-b733e338.pth', + 'tf_efficientnet_lite4': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_efficientnet_lite4-741542c3.pth', + + 'mixnet_s': 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/mixnet_s-a907afbc.pth', + 'mixnet_m': 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/mixnet_m-4647fc68.pth', + 'mixnet_l': 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/mixnet_l-5a9a2ed8.pth', + 'mixnet_xl': 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/mixnet_xl_ra-aac3c00c.pth', + + 'tf_mixnet_s': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_mixnet_s-89d3354b.pth', + 'tf_mixnet_m': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_mixnet_m-0f4d8805.pth', + 'tf_mixnet_l': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_mixnet_l-6c92e0c8.pth', +} + + +class GenEfficientNet(nn.Module): + """ Generic EfficientNets + + An implementation of mobile optimized networks that covers: + * EfficientNet (B0-B8, L2, CondConv, EdgeTPU) + * MixNet (Small, Medium, and Large, XL) + * MNASNet A1, B1, and small + * FBNet C + * Single-Path NAS Pixel1 + """ + + def __init__(self, block_args, num_classes=1000, in_chans=3, num_features=1280, stem_size=32, fix_stem=False, + channel_multiplier=1.0, channel_divisor=8, channel_min=None, + pad_type='', act_layer=nn.ReLU, drop_rate=0., drop_connect_rate=0., + se_kwargs=None, norm_layer=nn.BatchNorm2d, norm_kwargs=None, + weight_init='goog'): + super(GenEfficientNet, self).__init__() + self.drop_rate = drop_rate + + if not fix_stem: + stem_size = round_channels(stem_size, channel_multiplier, channel_divisor, channel_min) + self.conv_stem = select_conv2d(in_chans, stem_size, 3, stride=2, padding=pad_type) + self.bn1 = norm_layer(stem_size, **norm_kwargs) + self.act1 = act_layer(inplace=True) + in_chs = stem_size + + builder = EfficientNetBuilder( + channel_multiplier, channel_divisor, channel_min, + pad_type, act_layer, se_kwargs, norm_layer, norm_kwargs, drop_connect_rate) + self.blocks = nn.Sequential(*builder(in_chs, block_args)) + in_chs = builder.in_chs + + self.conv_head = select_conv2d(in_chs, num_features, 1, padding=pad_type) + self.bn2 = norm_layer(num_features, **norm_kwargs) + self.act2 = act_layer(inplace=True) + self.global_pool = nn.AdaptiveAvgPool2d(1) + self.classifier = nn.Linear(num_features, num_classes) + + for n, m in self.named_modules(): + if weight_init == 'goog': + initialize_weight_goog(m, n) + else: + initialize_weight_default(m, n) + + def features(self, x): + x = self.conv_stem(x) + x = self.bn1(x) + x = self.act1(x) + x = self.blocks(x) + x = self.conv_head(x) + x = self.bn2(x) + x = self.act2(x) + return x + + def as_sequential(self): + layers = [self.conv_stem, self.bn1, self.act1] + layers.extend(self.blocks) + layers.extend([ + self.conv_head, self.bn2, self.act2, + self.global_pool, nn.Flatten(), nn.Dropout(self.drop_rate), self.classifier]) + return nn.Sequential(*layers) + + def forward(self, x): + x = self.features(x) + x = self.global_pool(x) + x = x.flatten(1) + if self.drop_rate > 0.: + x = F.dropout(x, p=self.drop_rate, training=self.training) + return self.classifier(x) + + +def _create_model(model_kwargs, variant, pretrained=False): + as_sequential = model_kwargs.pop('as_sequential', False) + model = GenEfficientNet(**model_kwargs) + if pretrained: + load_pretrained(model, model_urls[variant]) + if as_sequential: + model = model.as_sequential() + return model + + +def _gen_mnasnet_a1(variant, channel_multiplier=1.0, pretrained=False, **kwargs): + """Creates a mnasnet-a1 model. + + Ref impl: https://github.com/tensorflow/tpu/tree/master/models/official/mnasnet + Paper: https://arxiv.org/pdf/1807.11626.pdf. + + Args: + channel_multiplier: multiplier to number of channels per layer. + """ + arch_def = [ + # stage 0, 112x112 in + ['ds_r1_k3_s1_e1_c16_noskip'], + # stage 1, 112x112 in + ['ir_r2_k3_s2_e6_c24'], + # stage 2, 56x56 in + ['ir_r3_k5_s2_e3_c40_se0.25'], + # stage 3, 28x28 in + ['ir_r4_k3_s2_e6_c80'], + # stage 4, 14x14in + ['ir_r2_k3_s1_e6_c112_se0.25'], + # stage 5, 14x14in + ['ir_r3_k5_s2_e6_c160_se0.25'], + # stage 6, 7x7 in + ['ir_r1_k3_s1_e6_c320'], + ] + with layer_config_kwargs(kwargs): + model_kwargs = dict( + block_args=decode_arch_def(arch_def), + stem_size=32, + channel_multiplier=channel_multiplier, + act_layer=resolve_act_layer(kwargs, 'relu'), + norm_kwargs=resolve_bn_args(kwargs), + **kwargs + ) + model = _create_model(model_kwargs, variant, pretrained) + return model + + +def _gen_mnasnet_b1(variant, channel_multiplier=1.0, pretrained=False, **kwargs): + """Creates a mnasnet-b1 model. + + Ref impl: https://github.com/tensorflow/tpu/tree/master/models/official/mnasnet + Paper: https://arxiv.org/pdf/1807.11626.pdf. + + Args: + channel_multiplier: multiplier to number of channels per layer. + """ + arch_def = [ + # stage 0, 112x112 in + ['ds_r1_k3_s1_c16_noskip'], + # stage 1, 112x112 in + ['ir_r3_k3_s2_e3_c24'], + # stage 2, 56x56 in + ['ir_r3_k5_s2_e3_c40'], + # stage 3, 28x28 in + ['ir_r3_k5_s2_e6_c80'], + # stage 4, 14x14in + ['ir_r2_k3_s1_e6_c96'], + # stage 5, 14x14in + ['ir_r4_k5_s2_e6_c192'], + # stage 6, 7x7 in + ['ir_r1_k3_s1_e6_c320_noskip'] + ] + with layer_config_kwargs(kwargs): + model_kwargs = dict( + block_args=decode_arch_def(arch_def), + stem_size=32, + channel_multiplier=channel_multiplier, + act_layer=resolve_act_layer(kwargs, 'relu'), + norm_kwargs=resolve_bn_args(kwargs), + **kwargs + ) + model = _create_model(model_kwargs, variant, pretrained) + return model + + +def _gen_mnasnet_small(variant, channel_multiplier=1.0, pretrained=False, **kwargs): + """Creates a mnasnet-b1 model. + + Ref impl: https://github.com/tensorflow/tpu/tree/master/models/official/mnasnet + Paper: https://arxiv.org/pdf/1807.11626.pdf. + + Args: + channel_multiplier: multiplier to number of channels per layer. + """ + arch_def = [ + ['ds_r1_k3_s1_c8'], + ['ir_r1_k3_s2_e3_c16'], + ['ir_r2_k3_s2_e6_c16'], + ['ir_r4_k5_s2_e6_c32_se0.25'], + ['ir_r3_k3_s1_e6_c32_se0.25'], + ['ir_r3_k5_s2_e6_c88_se0.25'], + ['ir_r1_k3_s1_e6_c144'] + ] + with layer_config_kwargs(kwargs): + model_kwargs = dict( + block_args=decode_arch_def(arch_def), + stem_size=8, + channel_multiplier=channel_multiplier, + act_layer=resolve_act_layer(kwargs, 'relu'), + norm_kwargs=resolve_bn_args(kwargs), + **kwargs + ) + model = _create_model(model_kwargs, variant, pretrained) + return model + + +def _gen_mobilenet_v2( + variant, channel_multiplier=1.0, depth_multiplier=1.0, fix_stem_head=False, pretrained=False, **kwargs): + """ Generate MobileNet-V2 network + Ref impl: https://github.com/tensorflow/models/blob/master/research/slim/nets/mobilenet/mobilenet_v2.py + Paper: https://arxiv.org/abs/1801.04381 + """ + arch_def = [ + ['ds_r1_k3_s1_c16'], + ['ir_r2_k3_s2_e6_c24'], + ['ir_r3_k3_s2_e6_c32'], + ['ir_r4_k3_s2_e6_c64'], + ['ir_r3_k3_s1_e6_c96'], + ['ir_r3_k3_s2_e6_c160'], + ['ir_r1_k3_s1_e6_c320'], + ] + with layer_config_kwargs(kwargs): + model_kwargs = dict( + block_args=decode_arch_def(arch_def, depth_multiplier=depth_multiplier, fix_first_last=fix_stem_head), + num_features=1280 if fix_stem_head else round_channels(1280, channel_multiplier, 8, None), + stem_size=32, + fix_stem=fix_stem_head, + channel_multiplier=channel_multiplier, + norm_kwargs=resolve_bn_args(kwargs), + act_layer=nn.ReLU6, + **kwargs + ) + model = _create_model(model_kwargs, variant, pretrained) + return model + + +def _gen_fbnetc(variant, channel_multiplier=1.0, pretrained=False, **kwargs): + """ FBNet-C + + Paper: https://arxiv.org/abs/1812.03443 + Ref Impl: https://github.com/facebookresearch/maskrcnn-benchmark/blob/master/maskrcnn_benchmark/modeling/backbone/fbnet_modeldef.py + + NOTE: the impl above does not relate to the 'C' variant here, that was derived from paper, + it was used to confirm some building block details + """ + arch_def = [ + ['ir_r1_k3_s1_e1_c16'], + ['ir_r1_k3_s2_e6_c24', 'ir_r2_k3_s1_e1_c24'], + ['ir_r1_k5_s2_e6_c32', 'ir_r1_k5_s1_e3_c32', 'ir_r1_k5_s1_e6_c32', 'ir_r1_k3_s1_e6_c32'], + ['ir_r1_k5_s2_e6_c64', 'ir_r1_k5_s1_e3_c64', 'ir_r2_k5_s1_e6_c64'], + ['ir_r3_k5_s1_e6_c112', 'ir_r1_k5_s1_e3_c112'], + ['ir_r4_k5_s2_e6_c184'], + ['ir_r1_k3_s1_e6_c352'], + ] + with layer_config_kwargs(kwargs): + model_kwargs = dict( + block_args=decode_arch_def(arch_def), + stem_size=16, + num_features=1984, # paper suggests this, but is not 100% clear + channel_multiplier=channel_multiplier, + act_layer=resolve_act_layer(kwargs, 'relu'), + norm_kwargs=resolve_bn_args(kwargs), + **kwargs + ) + model = _create_model(model_kwargs, variant, pretrained) + return model + + +def _gen_spnasnet(variant, channel_multiplier=1.0, pretrained=False, **kwargs): + """Creates the Single-Path NAS model from search targeted for Pixel1 phone. + + Paper: https://arxiv.org/abs/1904.02877 + + Args: + channel_multiplier: multiplier to number of channels per layer. + """ + arch_def = [ + # stage 0, 112x112 in + ['ds_r1_k3_s1_c16_noskip'], + # stage 1, 112x112 in + ['ir_r3_k3_s2_e3_c24'], + # stage 2, 56x56 in + ['ir_r1_k5_s2_e6_c40', 'ir_r3_k3_s1_e3_c40'], + # stage 3, 28x28 in + ['ir_r1_k5_s2_e6_c80', 'ir_r3_k3_s1_e3_c80'], + # stage 4, 14x14in + ['ir_r1_k5_s1_e6_c96', 'ir_r3_k5_s1_e3_c96'], + # stage 5, 14x14in + ['ir_r4_k5_s2_e6_c192'], + # stage 6, 7x7 in + ['ir_r1_k3_s1_e6_c320_noskip'] + ] + with layer_config_kwargs(kwargs): + model_kwargs = dict( + block_args=decode_arch_def(arch_def), + stem_size=32, + channel_multiplier=channel_multiplier, + act_layer=resolve_act_layer(kwargs, 'relu'), + norm_kwargs=resolve_bn_args(kwargs), + **kwargs + ) + model = _create_model(model_kwargs, variant, pretrained) + return model + + +def _gen_efficientnet(variant, channel_multiplier=1.0, depth_multiplier=1.0, pretrained=False, **kwargs): + """Creates an EfficientNet model. + + Ref impl: https://github.com/tensorflow/tpu/blob/master/models/official/efficientnet/efficientnet_model.py + Paper: https://arxiv.org/abs/1905.11946 + + EfficientNet params + name: (channel_multiplier, depth_multiplier, resolution, dropout_rate) + 'efficientnet-b0': (1.0, 1.0, 224, 0.2), + 'efficientnet-b1': (1.0, 1.1, 240, 0.2), + 'efficientnet-b2': (1.1, 1.2, 260, 0.3), + 'efficientnet-b3': (1.2, 1.4, 300, 0.3), + 'efficientnet-b4': (1.4, 1.8, 380, 0.4), + 'efficientnet-b5': (1.6, 2.2, 456, 0.4), + 'efficientnet-b6': (1.8, 2.6, 528, 0.5), + 'efficientnet-b7': (2.0, 3.1, 600, 0.5), + 'efficientnet-b8': (2.2, 3.6, 672, 0.5), + + Args: + channel_multiplier: multiplier to number of channels per layer + depth_multiplier: multiplier to number of repeats per stage + + """ + arch_def = [ + ['ds_r1_k3_s1_e1_c16_se0.25'], + ['ir_r2_k3_s2_e6_c24_se0.25'], + ['ir_r2_k5_s2_e6_c40_se0.25'], + ['ir_r3_k3_s2_e6_c80_se0.25'], + ['ir_r3_k5_s1_e6_c112_se0.25'], + ['ir_r4_k5_s2_e6_c192_se0.25'], + ['ir_r1_k3_s1_e6_c320_se0.25'], + ] + with layer_config_kwargs(kwargs): + model_kwargs = dict( + block_args=decode_arch_def(arch_def, depth_multiplier), + num_features=round_channels(1280, channel_multiplier, 8, None), + stem_size=32, + channel_multiplier=channel_multiplier, + act_layer=resolve_act_layer(kwargs, 'swish'), + norm_kwargs=resolve_bn_args(kwargs), + **kwargs, + ) + model = _create_model(model_kwargs, variant, pretrained) + return model + + +def _gen_efficientnet_edge(variant, channel_multiplier=1.0, depth_multiplier=1.0, pretrained=False, **kwargs): + arch_def = [ + # NOTE `fc` is present to override a mismatch between stem channels and in chs not + # present in other models + ['er_r1_k3_s1_e4_c24_fc24_noskip'], + ['er_r2_k3_s2_e8_c32'], + ['er_r4_k3_s2_e8_c48'], + ['ir_r5_k5_s2_e8_c96'], + ['ir_r4_k5_s1_e8_c144'], + ['ir_r2_k5_s2_e8_c192'], + ] + with layer_config_kwargs(kwargs): + model_kwargs = dict( + block_args=decode_arch_def(arch_def, depth_multiplier), + num_features=round_channels(1280, channel_multiplier, 8, None), + stem_size=32, + channel_multiplier=channel_multiplier, + act_layer=resolve_act_layer(kwargs, 'relu'), + norm_kwargs=resolve_bn_args(kwargs), + **kwargs, + ) + model = _create_model(model_kwargs, variant, pretrained) + return model + + +def _gen_efficientnet_condconv( + variant, channel_multiplier=1.0, depth_multiplier=1.0, experts_multiplier=1, pretrained=False, **kwargs): + """Creates an efficientnet-condconv model.""" + arch_def = [ + ['ds_r1_k3_s1_e1_c16_se0.25'], + ['ir_r2_k3_s2_e6_c24_se0.25'], + ['ir_r2_k5_s2_e6_c40_se0.25'], + ['ir_r3_k3_s2_e6_c80_se0.25'], + ['ir_r3_k5_s1_e6_c112_se0.25_cc4'], + ['ir_r4_k5_s2_e6_c192_se0.25_cc4'], + ['ir_r1_k3_s1_e6_c320_se0.25_cc4'], + ] + with layer_config_kwargs(kwargs): + model_kwargs = dict( + block_args=decode_arch_def(arch_def, depth_multiplier, experts_multiplier=experts_multiplier), + num_features=round_channels(1280, channel_multiplier, 8, None), + stem_size=32, + channel_multiplier=channel_multiplier, + act_layer=resolve_act_layer(kwargs, 'swish'), + norm_kwargs=resolve_bn_args(kwargs), + **kwargs, + ) + model = _create_model(model_kwargs, variant, pretrained) + return model + + +def _gen_efficientnet_lite(variant, channel_multiplier=1.0, depth_multiplier=1.0, pretrained=False, **kwargs): + """Creates an EfficientNet-Lite model. + + Ref impl: https://github.com/tensorflow/tpu/tree/master/models/official/efficientnet/lite + Paper: https://arxiv.org/abs/1905.11946 + + EfficientNet params + name: (channel_multiplier, depth_multiplier, resolution, dropout_rate) + 'efficientnet-lite0': (1.0, 1.0, 224, 0.2), + 'efficientnet-lite1': (1.0, 1.1, 240, 0.2), + 'efficientnet-lite2': (1.1, 1.2, 260, 0.3), + 'efficientnet-lite3': (1.2, 1.4, 280, 0.3), + 'efficientnet-lite4': (1.4, 1.8, 300, 0.3), + + Args: + channel_multiplier: multiplier to number of channels per layer + depth_multiplier: multiplier to number of repeats per stage + """ + arch_def = [ + ['ds_r1_k3_s1_e1_c16'], + ['ir_r2_k3_s2_e6_c24'], + ['ir_r2_k5_s2_e6_c40'], + ['ir_r3_k3_s2_e6_c80'], + ['ir_r3_k5_s1_e6_c112'], + ['ir_r4_k5_s2_e6_c192'], + ['ir_r1_k3_s1_e6_c320'], + ] + with layer_config_kwargs(kwargs): + model_kwargs = dict( + block_args=decode_arch_def(arch_def, depth_multiplier, fix_first_last=True), + num_features=1280, + stem_size=32, + fix_stem=True, + channel_multiplier=channel_multiplier, + act_layer=nn.ReLU6, + norm_kwargs=resolve_bn_args(kwargs), + **kwargs, + ) + model = _create_model(model_kwargs, variant, pretrained) + return model + + +def _gen_mixnet_s(variant, channel_multiplier=1.0, pretrained=False, **kwargs): + """Creates a MixNet Small model. + + Ref impl: https://github.com/tensorflow/tpu/tree/master/models/official/mnasnet/mixnet + Paper: https://arxiv.org/abs/1907.09595 + """ + arch_def = [ + # stage 0, 112x112 in + ['ds_r1_k3_s1_e1_c16'], # relu + # stage 1, 112x112 in + ['ir_r1_k3_a1.1_p1.1_s2_e6_c24', 'ir_r1_k3_a1.1_p1.1_s1_e3_c24'], # relu + # stage 2, 56x56 in + ['ir_r1_k3.5.7_s2_e6_c40_se0.5_nsw', 'ir_r3_k3.5_a1.1_p1.1_s1_e6_c40_se0.5_nsw'], # swish + # stage 3, 28x28 in + ['ir_r1_k3.5.7_p1.1_s2_e6_c80_se0.25_nsw', 'ir_r2_k3.5_p1.1_s1_e6_c80_se0.25_nsw'], # swish + # stage 4, 14x14in + ['ir_r1_k3.5.7_a1.1_p1.1_s1_e6_c120_se0.5_nsw', 'ir_r2_k3.5.7.9_a1.1_p1.1_s1_e3_c120_se0.5_nsw'], # swish + # stage 5, 14x14in + ['ir_r1_k3.5.7.9.11_s2_e6_c200_se0.5_nsw', 'ir_r2_k3.5.7.9_p1.1_s1_e6_c200_se0.5_nsw'], # swish + # 7x7 + ] + with layer_config_kwargs(kwargs): + model_kwargs = dict( + block_args=decode_arch_def(arch_def), + num_features=1536, + stem_size=16, + channel_multiplier=channel_multiplier, + act_layer=resolve_act_layer(kwargs, 'relu'), + norm_kwargs=resolve_bn_args(kwargs), + **kwargs + ) + model = _create_model(model_kwargs, variant, pretrained) + return model + + +def _gen_mixnet_m(variant, channel_multiplier=1.0, depth_multiplier=1.0, pretrained=False, **kwargs): + """Creates a MixNet Medium-Large model. + + Ref impl: https://github.com/tensorflow/tpu/tree/master/models/official/mnasnet/mixnet + Paper: https://arxiv.org/abs/1907.09595 + """ + arch_def = [ + # stage 0, 112x112 in + ['ds_r1_k3_s1_e1_c24'], # relu + # stage 1, 112x112 in + ['ir_r1_k3.5.7_a1.1_p1.1_s2_e6_c32', 'ir_r1_k3_a1.1_p1.1_s1_e3_c32'], # relu + # stage 2, 56x56 in + ['ir_r1_k3.5.7.9_s2_e6_c40_se0.5_nsw', 'ir_r3_k3.5_a1.1_p1.1_s1_e6_c40_se0.5_nsw'], # swish + # stage 3, 28x28 in + ['ir_r1_k3.5.7_s2_e6_c80_se0.25_nsw', 'ir_r3_k3.5.7.9_a1.1_p1.1_s1_e6_c80_se0.25_nsw'], # swish + # stage 4, 14x14in + ['ir_r1_k3_s1_e6_c120_se0.5_nsw', 'ir_r3_k3.5.7.9_a1.1_p1.1_s1_e3_c120_se0.5_nsw'], # swish + # stage 5, 14x14in + ['ir_r1_k3.5.7.9_s2_e6_c200_se0.5_nsw', 'ir_r3_k3.5.7.9_p1.1_s1_e6_c200_se0.5_nsw'], # swish + # 7x7 + ] + with layer_config_kwargs(kwargs): + model_kwargs = dict( + block_args=decode_arch_def(arch_def, depth_multiplier, depth_trunc='round'), + num_features=1536, + stem_size=24, + channel_multiplier=channel_multiplier, + act_layer=resolve_act_layer(kwargs, 'relu'), + norm_kwargs=resolve_bn_args(kwargs), + **kwargs + ) + model = _create_model(model_kwargs, variant, pretrained) + return model + + +def mnasnet_050(pretrained=False, **kwargs): + """ MNASNet B1, depth multiplier of 0.5. """ + model = _gen_mnasnet_b1('mnasnet_050', 0.5, pretrained=pretrained, **kwargs) + return model + + +def mnasnet_075(pretrained=False, **kwargs): + """ MNASNet B1, depth multiplier of 0.75. """ + model = _gen_mnasnet_b1('mnasnet_075', 0.75, pretrained=pretrained, **kwargs) + return model + + +def mnasnet_100(pretrained=False, **kwargs): + """ MNASNet B1, depth multiplier of 1.0. """ + model = _gen_mnasnet_b1('mnasnet_100', 1.0, pretrained=pretrained, **kwargs) + return model + + +def mnasnet_b1(pretrained=False, **kwargs): + """ MNASNet B1, depth multiplier of 1.0. """ + return mnasnet_100(pretrained, **kwargs) + + +def mnasnet_140(pretrained=False, **kwargs): + """ MNASNet B1, depth multiplier of 1.4 """ + model = _gen_mnasnet_b1('mnasnet_140', 1.4, pretrained=pretrained, **kwargs) + return model + + +def semnasnet_050(pretrained=False, **kwargs): + """ MNASNet A1 (w/ SE), depth multiplier of 0.5 """ + model = _gen_mnasnet_a1('semnasnet_050', 0.5, pretrained=pretrained, **kwargs) + return model + + +def semnasnet_075(pretrained=False, **kwargs): + """ MNASNet A1 (w/ SE), depth multiplier of 0.75. """ + model = _gen_mnasnet_a1('semnasnet_075', 0.75, pretrained=pretrained, **kwargs) + return model + + +def semnasnet_100(pretrained=False, **kwargs): + """ MNASNet A1 (w/ SE), depth multiplier of 1.0. """ + model = _gen_mnasnet_a1('semnasnet_100', 1.0, pretrained=pretrained, **kwargs) + return model + + +def mnasnet_a1(pretrained=False, **kwargs): + """ MNASNet A1 (w/ SE), depth multiplier of 1.0. """ + return semnasnet_100(pretrained, **kwargs) + + +def semnasnet_140(pretrained=False, **kwargs): + """ MNASNet A1 (w/ SE), depth multiplier of 1.4. """ + model = _gen_mnasnet_a1('semnasnet_140', 1.4, pretrained=pretrained, **kwargs) + return model + + +def mnasnet_small(pretrained=False, **kwargs): + """ MNASNet Small, depth multiplier of 1.0. """ + model = _gen_mnasnet_small('mnasnet_small', 1.0, pretrained=pretrained, **kwargs) + return model + + +def mobilenetv2_100(pretrained=False, **kwargs): + """ MobileNet V2 w/ 1.0 channel multiplier """ + model = _gen_mobilenet_v2('mobilenetv2_100', 1.0, pretrained=pretrained, **kwargs) + return model + + +def mobilenetv2_140(pretrained=False, **kwargs): + """ MobileNet V2 w/ 1.4 channel multiplier """ + model = _gen_mobilenet_v2('mobilenetv2_140', 1.4, pretrained=pretrained, **kwargs) + return model + + +def mobilenetv2_110d(pretrained=False, **kwargs): + """ MobileNet V2 w/ 1.1 channel, 1.2 depth multipliers""" + model = _gen_mobilenet_v2( + 'mobilenetv2_110d', 1.1, depth_multiplier=1.2, fix_stem_head=True, pretrained=pretrained, **kwargs) + return model + + +def mobilenetv2_120d(pretrained=False, **kwargs): + """ MobileNet V2 w/ 1.2 channel, 1.4 depth multipliers """ + model = _gen_mobilenet_v2( + 'mobilenetv2_120d', 1.2, depth_multiplier=1.4, fix_stem_head=True, pretrained=pretrained, **kwargs) + return model + + +def fbnetc_100(pretrained=False, **kwargs): + """ FBNet-C """ + if pretrained: + # pretrained model trained with non-default BN epsilon + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + model = _gen_fbnetc('fbnetc_100', 1.0, pretrained=pretrained, **kwargs) + return model + + +def spnasnet_100(pretrained=False, **kwargs): + """ Single-Path NAS Pixel1""" + model = _gen_spnasnet('spnasnet_100', 1.0, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_b0(pretrained=False, **kwargs): + """ EfficientNet-B0 """ + # NOTE for train set drop_rate=0.2, drop_connect_rate=0.2 + model = _gen_efficientnet( + 'efficientnet_b0', channel_multiplier=1.0, depth_multiplier=1.0, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_b1(pretrained=False, **kwargs): + """ EfficientNet-B1 """ + # NOTE for train set drop_rate=0.2, drop_connect_rate=0.2 + model = _gen_efficientnet( + 'efficientnet_b1', channel_multiplier=1.0, depth_multiplier=1.1, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_b2(pretrained=False, **kwargs): + """ EfficientNet-B2 """ + # NOTE for train set drop_rate=0.3, drop_connect_rate=0.2 + model = _gen_efficientnet( + 'efficientnet_b2', channel_multiplier=1.1, depth_multiplier=1.2, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_b3(pretrained=False, **kwargs): + """ EfficientNet-B3 """ + # NOTE for train set drop_rate=0.3, drop_connect_rate=0.2 + model = _gen_efficientnet( + 'efficientnet_b3', channel_multiplier=1.2, depth_multiplier=1.4, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_b4(pretrained=False, **kwargs): + """ EfficientNet-B4 """ + # NOTE for train set drop_rate=0.4, drop_connect_rate=0.2 + model = _gen_efficientnet( + 'efficientnet_b4', channel_multiplier=1.4, depth_multiplier=1.8, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_b5(pretrained=False, **kwargs): + """ EfficientNet-B5 """ + # NOTE for train set drop_rate=0.4, drop_connect_rate=0.2 + model = _gen_efficientnet( + 'efficientnet_b5', channel_multiplier=1.6, depth_multiplier=2.2, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_b6(pretrained=False, **kwargs): + """ EfficientNet-B6 """ + # NOTE for train set drop_rate=0.5, drop_connect_rate=0.2 + model = _gen_efficientnet( + 'efficientnet_b6', channel_multiplier=1.8, depth_multiplier=2.6, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_b7(pretrained=False, **kwargs): + """ EfficientNet-B7 """ + # NOTE for train set drop_rate=0.5, drop_connect_rate=0.2 + model = _gen_efficientnet( + 'efficientnet_b7', channel_multiplier=2.0, depth_multiplier=3.1, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_b8(pretrained=False, **kwargs): + """ EfficientNet-B8 """ + # NOTE for train set drop_rate=0.5, drop_connect_rate=0.2 + model = _gen_efficientnet( + 'efficientnet_b8', channel_multiplier=2.2, depth_multiplier=3.6, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_l2(pretrained=False, **kwargs): + """ EfficientNet-L2. """ + # NOTE for train, drop_rate should be 0.5 + model = _gen_efficientnet( + 'efficientnet_l2', channel_multiplier=4.3, depth_multiplier=5.3, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_es(pretrained=False, **kwargs): + """ EfficientNet-Edge Small. """ + model = _gen_efficientnet_edge( + 'efficientnet_es', channel_multiplier=1.0, depth_multiplier=1.0, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_em(pretrained=False, **kwargs): + """ EfficientNet-Edge-Medium. """ + model = _gen_efficientnet_edge( + 'efficientnet_em', channel_multiplier=1.0, depth_multiplier=1.1, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_el(pretrained=False, **kwargs): + """ EfficientNet-Edge-Large. """ + model = _gen_efficientnet_edge( + 'efficientnet_el', channel_multiplier=1.2, depth_multiplier=1.4, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_cc_b0_4e(pretrained=False, **kwargs): + """ EfficientNet-CondConv-B0 w/ 8 Experts """ + # NOTE for train set drop_rate=0.25, drop_connect_rate=0.2 + model = _gen_efficientnet_condconv( + 'efficientnet_cc_b0_4e', channel_multiplier=1.0, depth_multiplier=1.0, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_cc_b0_8e(pretrained=False, **kwargs): + """ EfficientNet-CondConv-B0 w/ 8 Experts """ + # NOTE for train set drop_rate=0.25, drop_connect_rate=0.2 + model = _gen_efficientnet_condconv( + 'efficientnet_cc_b0_8e', channel_multiplier=1.0, depth_multiplier=1.0, experts_multiplier=2, + pretrained=pretrained, **kwargs) + return model + + +def efficientnet_cc_b1_8e(pretrained=False, **kwargs): + """ EfficientNet-CondConv-B1 w/ 8 Experts """ + # NOTE for train set drop_rate=0.25, drop_connect_rate=0.2 + model = _gen_efficientnet_condconv( + 'efficientnet_cc_b1_8e', channel_multiplier=1.0, depth_multiplier=1.1, experts_multiplier=2, + pretrained=pretrained, **kwargs) + return model + + +def efficientnet_lite0(pretrained=False, **kwargs): + """ EfficientNet-Lite0 """ + model = _gen_efficientnet_lite( + 'efficientnet_lite0', channel_multiplier=1.0, depth_multiplier=1.0, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_lite1(pretrained=False, **kwargs): + """ EfficientNet-Lite1 """ + model = _gen_efficientnet_lite( + 'efficientnet_lite1', channel_multiplier=1.0, depth_multiplier=1.1, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_lite2(pretrained=False, **kwargs): + """ EfficientNet-Lite2 """ + model = _gen_efficientnet_lite( + 'efficientnet_lite2', channel_multiplier=1.1, depth_multiplier=1.2, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_lite3(pretrained=False, **kwargs): + """ EfficientNet-Lite3 """ + model = _gen_efficientnet_lite( + 'efficientnet_lite3', channel_multiplier=1.2, depth_multiplier=1.4, pretrained=pretrained, **kwargs) + return model + + +def efficientnet_lite4(pretrained=False, **kwargs): + """ EfficientNet-Lite4 """ + model = _gen_efficientnet_lite( + 'efficientnet_lite4', channel_multiplier=1.4, depth_multiplier=1.8, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b0(pretrained=False, **kwargs): + """ EfficientNet-B0 AutoAug. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b0', channel_multiplier=1.0, depth_multiplier=1.0, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b1(pretrained=False, **kwargs): + """ EfficientNet-B1 AutoAug. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b1', channel_multiplier=1.0, depth_multiplier=1.1, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b2(pretrained=False, **kwargs): + """ EfficientNet-B2 AutoAug. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b2', channel_multiplier=1.1, depth_multiplier=1.2, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b3(pretrained=False, **kwargs): + """ EfficientNet-B3 AutoAug. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b3', channel_multiplier=1.2, depth_multiplier=1.4, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b4(pretrained=False, **kwargs): + """ EfficientNet-B4 AutoAug. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b4', channel_multiplier=1.4, depth_multiplier=1.8, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b5(pretrained=False, **kwargs): + """ EfficientNet-B5 RandAug. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b5', channel_multiplier=1.6, depth_multiplier=2.2, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b6(pretrained=False, **kwargs): + """ EfficientNet-B6 AutoAug. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b6', channel_multiplier=1.8, depth_multiplier=2.6, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b7(pretrained=False, **kwargs): + """ EfficientNet-B7 RandAug. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b7', channel_multiplier=2.0, depth_multiplier=3.1, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b8(pretrained=False, **kwargs): + """ EfficientNet-B8 RandAug. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b8', channel_multiplier=2.2, depth_multiplier=3.6, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b0_ap(pretrained=False, **kwargs): + """ EfficientNet-B0 AdvProp. Tensorflow compatible variant + Paper: Adversarial Examples Improve Image Recognition (https://arxiv.org/abs/1911.09665) + """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b0_ap', channel_multiplier=1.0, depth_multiplier=1.0, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b1_ap(pretrained=False, **kwargs): + """ EfficientNet-B1 AdvProp. Tensorflow compatible variant + Paper: Adversarial Examples Improve Image Recognition (https://arxiv.org/abs/1911.09665) + """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b1_ap', channel_multiplier=1.0, depth_multiplier=1.1, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b2_ap(pretrained=False, **kwargs): + """ EfficientNet-B2 AdvProp. Tensorflow compatible variant + Paper: Adversarial Examples Improve Image Recognition (https://arxiv.org/abs/1911.09665) + """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b2_ap', channel_multiplier=1.1, depth_multiplier=1.2, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b3_ap(pretrained=False, **kwargs): + """ EfficientNet-B3 AdvProp. Tensorflow compatible variant + Paper: Adversarial Examples Improve Image Recognition (https://arxiv.org/abs/1911.09665) + """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b3_ap', channel_multiplier=1.2, depth_multiplier=1.4, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b4_ap(pretrained=False, **kwargs): + """ EfficientNet-B4 AdvProp. Tensorflow compatible variant + Paper: Adversarial Examples Improve Image Recognition (https://arxiv.org/abs/1911.09665) + """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b4_ap', channel_multiplier=1.4, depth_multiplier=1.8, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b5_ap(pretrained=False, **kwargs): + """ EfficientNet-B5 AdvProp. Tensorflow compatible variant + Paper: Adversarial Examples Improve Image Recognition (https://arxiv.org/abs/1911.09665) + """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b5_ap', channel_multiplier=1.6, depth_multiplier=2.2, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b6_ap(pretrained=False, **kwargs): + """ EfficientNet-B6 AdvProp. Tensorflow compatible variant + Paper: Adversarial Examples Improve Image Recognition (https://arxiv.org/abs/1911.09665) + """ + # NOTE for train, drop_rate should be 0.5 + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b6_ap', channel_multiplier=1.8, depth_multiplier=2.6, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b7_ap(pretrained=False, **kwargs): + """ EfficientNet-B7 AdvProp. Tensorflow compatible variant + Paper: Adversarial Examples Improve Image Recognition (https://arxiv.org/abs/1911.09665) + """ + # NOTE for train, drop_rate should be 0.5 + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b7_ap', channel_multiplier=2.0, depth_multiplier=3.1, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b8_ap(pretrained=False, **kwargs): + """ EfficientNet-B8 AdvProp. Tensorflow compatible variant + Paper: Adversarial Examples Improve Image Recognition (https://arxiv.org/abs/1911.09665) + """ + # NOTE for train, drop_rate should be 0.5 + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b8_ap', channel_multiplier=2.2, depth_multiplier=3.6, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b0_ns(pretrained=False, **kwargs): + """ EfficientNet-B0 NoisyStudent. Tensorflow compatible variant + Paper: Self-training with Noisy Student improves ImageNet classification (https://arxiv.org/abs/1911.04252) + """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b0_ns', channel_multiplier=1.0, depth_multiplier=1.0, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b1_ns(pretrained=False, **kwargs): + """ EfficientNet-B1 NoisyStudent. Tensorflow compatible variant + Paper: Self-training with Noisy Student improves ImageNet classification (https://arxiv.org/abs/1911.04252) + """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b1_ns', channel_multiplier=1.0, depth_multiplier=1.1, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b2_ns(pretrained=False, **kwargs): + """ EfficientNet-B2 NoisyStudent. Tensorflow compatible variant + Paper: Self-training with Noisy Student improves ImageNet classification (https://arxiv.org/abs/1911.04252) + """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b2_ns', channel_multiplier=1.1, depth_multiplier=1.2, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b3_ns(pretrained=False, **kwargs): + """ EfficientNet-B3 NoisyStudent. Tensorflow compatible variant + Paper: Self-training with Noisy Student improves ImageNet classification (https://arxiv.org/abs/1911.04252) + """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b3_ns', channel_multiplier=1.2, depth_multiplier=1.4, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b4_ns(pretrained=False, **kwargs): + """ EfficientNet-B4 NoisyStudent. Tensorflow compatible variant + Paper: Self-training with Noisy Student improves ImageNet classification (https://arxiv.org/abs/1911.04252) + """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b4_ns', channel_multiplier=1.4, depth_multiplier=1.8, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b5_ns(pretrained=False, **kwargs): + """ EfficientNet-B5 NoisyStudent. Tensorflow compatible variant + Paper: Self-training with Noisy Student improves ImageNet classification (https://arxiv.org/abs/1911.04252) + """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b5_ns', channel_multiplier=1.6, depth_multiplier=2.2, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b6_ns(pretrained=False, **kwargs): + """ EfficientNet-B6 NoisyStudent. Tensorflow compatible variant + Paper: Self-training with Noisy Student improves ImageNet classification (https://arxiv.org/abs/1911.04252) + """ + # NOTE for train, drop_rate should be 0.5 + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b6_ns', channel_multiplier=1.8, depth_multiplier=2.6, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_b7_ns(pretrained=False, **kwargs): + """ EfficientNet-B7 NoisyStudent. Tensorflow compatible variant + Paper: Self-training with Noisy Student improves ImageNet classification (https://arxiv.org/abs/1911.04252) + """ + # NOTE for train, drop_rate should be 0.5 + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_b7_ns', channel_multiplier=2.0, depth_multiplier=3.1, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_l2_ns_475(pretrained=False, **kwargs): + """ EfficientNet-L2 NoisyStudent @ 475x475. Tensorflow compatible variant + Paper: Self-training with Noisy Student improves ImageNet classification (https://arxiv.org/abs/1911.04252) + """ + # NOTE for train, drop_rate should be 0.5 + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_l2_ns_475', channel_multiplier=4.3, depth_multiplier=5.3, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_l2_ns(pretrained=False, **kwargs): + """ EfficientNet-L2 NoisyStudent. Tensorflow compatible variant + Paper: Self-training with Noisy Student improves ImageNet classification (https://arxiv.org/abs/1911.04252) + """ + # NOTE for train, drop_rate should be 0.5 + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet( + 'tf_efficientnet_l2_ns', channel_multiplier=4.3, depth_multiplier=5.3, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_es(pretrained=False, **kwargs): + """ EfficientNet-Edge Small. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet_edge( + 'tf_efficientnet_es', channel_multiplier=1.0, depth_multiplier=1.0, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_em(pretrained=False, **kwargs): + """ EfficientNet-Edge-Medium. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet_edge( + 'tf_efficientnet_em', channel_multiplier=1.0, depth_multiplier=1.1, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_el(pretrained=False, **kwargs): + """ EfficientNet-Edge-Large. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet_edge( + 'tf_efficientnet_el', channel_multiplier=1.2, depth_multiplier=1.4, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_cc_b0_4e(pretrained=False, **kwargs): + """ EfficientNet-CondConv-B0 w/ 4 Experts """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet_condconv( + 'tf_efficientnet_cc_b0_4e', channel_multiplier=1.0, depth_multiplier=1.0, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_cc_b0_8e(pretrained=False, **kwargs): + """ EfficientNet-CondConv-B0 w/ 8 Experts """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet_condconv( + 'tf_efficientnet_cc_b0_8e', channel_multiplier=1.0, depth_multiplier=1.0, experts_multiplier=2, + pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_cc_b1_8e(pretrained=False, **kwargs): + """ EfficientNet-CondConv-B1 w/ 8 Experts """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet_condconv( + 'tf_efficientnet_cc_b1_8e', channel_multiplier=1.0, depth_multiplier=1.1, experts_multiplier=2, + pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_lite0(pretrained=False, **kwargs): + """ EfficientNet-Lite0. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet_lite( + 'tf_efficientnet_lite0', channel_multiplier=1.0, depth_multiplier=1.0, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_lite1(pretrained=False, **kwargs): + """ EfficientNet-Lite1. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet_lite( + 'tf_efficientnet_lite1', channel_multiplier=1.0, depth_multiplier=1.1, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_lite2(pretrained=False, **kwargs): + """ EfficientNet-Lite2. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet_lite( + 'tf_efficientnet_lite2', channel_multiplier=1.1, depth_multiplier=1.2, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_lite3(pretrained=False, **kwargs): + """ EfficientNet-Lite3. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet_lite( + 'tf_efficientnet_lite3', channel_multiplier=1.2, depth_multiplier=1.4, pretrained=pretrained, **kwargs) + return model + + +def tf_efficientnet_lite4(pretrained=False, **kwargs): + """ EfficientNet-Lite4. Tensorflow compatible variant """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_efficientnet_lite( + 'tf_efficientnet_lite4', channel_multiplier=1.4, depth_multiplier=1.8, pretrained=pretrained, **kwargs) + return model + + +def mixnet_s(pretrained=False, **kwargs): + """Creates a MixNet Small model. + """ + # NOTE for train set drop_rate=0.2 + model = _gen_mixnet_s( + 'mixnet_s', channel_multiplier=1.0, pretrained=pretrained, **kwargs) + return model + + +def mixnet_m(pretrained=False, **kwargs): + """Creates a MixNet Medium model. + """ + # NOTE for train set drop_rate=0.25 + model = _gen_mixnet_m( + 'mixnet_m', channel_multiplier=1.0, pretrained=pretrained, **kwargs) + return model + + +def mixnet_l(pretrained=False, **kwargs): + """Creates a MixNet Large model. + """ + # NOTE for train set drop_rate=0.25 + model = _gen_mixnet_m( + 'mixnet_l', channel_multiplier=1.3, pretrained=pretrained, **kwargs) + return model + + +def mixnet_xl(pretrained=False, **kwargs): + """Creates a MixNet Extra-Large model. + Not a paper spec, experimental def by RW w/ depth scaling. + """ + # NOTE for train set drop_rate=0.25, drop_connect_rate=0.2 + model = _gen_mixnet_m( + 'mixnet_xl', channel_multiplier=1.6, depth_multiplier=1.2, pretrained=pretrained, **kwargs) + return model + + +def mixnet_xxl(pretrained=False, **kwargs): + """Creates a MixNet Double Extra Large model. + Not a paper spec, experimental def by RW w/ depth scaling. + """ + # NOTE for train set drop_rate=0.3, drop_connect_rate=0.2 + model = _gen_mixnet_m( + 'mixnet_xxl', channel_multiplier=2.4, depth_multiplier=1.3, pretrained=pretrained, **kwargs) + return model + + +def tf_mixnet_s(pretrained=False, **kwargs): + """Creates a MixNet Small model. Tensorflow compatible variant + """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_mixnet_s( + 'tf_mixnet_s', channel_multiplier=1.0, pretrained=pretrained, **kwargs) + return model + + +def tf_mixnet_m(pretrained=False, **kwargs): + """Creates a MixNet Medium model. Tensorflow compatible variant + """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_mixnet_m( + 'tf_mixnet_m', channel_multiplier=1.0, pretrained=pretrained, **kwargs) + return model + + +def tf_mixnet_l(pretrained=False, **kwargs): + """Creates a MixNet Large model. Tensorflow compatible variant + """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_mixnet_m( + 'tf_mixnet_l', channel_multiplier=1.3, pretrained=pretrained, **kwargs) + return model diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/helpers.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/helpers.py new file mode 100644 index 00000000000..3f83a07d690 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/helpers.py @@ -0,0 +1,71 @@ +""" Checkpoint loading / state_dict helpers +Copyright 2020 Ross Wightman +""" +import torch +import os +from collections import OrderedDict +try: + from torch.hub import load_state_dict_from_url +except ImportError: + from torch.utils.model_zoo import load_url as load_state_dict_from_url + + +def load_checkpoint(model, checkpoint_path): + if checkpoint_path and os.path.isfile(checkpoint_path): + print("=> Loading checkpoint '{}'".format(checkpoint_path)) + checkpoint = torch.load(checkpoint_path) + if isinstance(checkpoint, dict) and 'state_dict' in checkpoint: + new_state_dict = OrderedDict() + for k, v in checkpoint['state_dict'].items(): + if k.startswith('module'): + name = k[7:] # remove `module.` + else: + name = k + new_state_dict[name] = v + model.load_state_dict(new_state_dict) + else: + model.load_state_dict(checkpoint) + print("=> Loaded checkpoint '{}'".format(checkpoint_path)) + else: + print("=> Error: No checkpoint found at '{}'".format(checkpoint_path)) + raise FileNotFoundError() + + +def load_pretrained(model, url, filter_fn=None, strict=True): + if not url: + print("=> Warning: Pretrained model URL is empty, using random initialization.") + return + + state_dict = load_state_dict_from_url(url, progress=False, map_location='cpu') + + input_conv = 'conv_stem' + classifier = 'classifier' + in_chans = getattr(model, input_conv).weight.shape[1] + num_classes = getattr(model, classifier).weight.shape[0] + + input_conv_weight = input_conv + '.weight' + pretrained_in_chans = state_dict[input_conv_weight].shape[1] + if in_chans != pretrained_in_chans: + if in_chans == 1: + print('=> Converting pretrained input conv {} from {} to 1 channel'.format( + input_conv_weight, pretrained_in_chans)) + conv1_weight = state_dict[input_conv_weight] + state_dict[input_conv_weight] = conv1_weight.sum(dim=1, keepdim=True) + else: + print('=> Discarding pretrained input conv {} since input channel count != {}'.format( + input_conv_weight, pretrained_in_chans)) + del state_dict[input_conv_weight] + strict = False + + classifier_weight = classifier + '.weight' + pretrained_num_classes = state_dict[classifier_weight].shape[0] + if num_classes != pretrained_num_classes: + print('=> Discarding pretrained classifier since num_classes != {}'.format(pretrained_num_classes)) + del state_dict[classifier_weight] + del state_dict[classifier + '.bias'] + strict = False + + if filter_fn is not None: + state_dict = filter_fn(state_dict) + + model.load_state_dict(state_dict, strict=strict) diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/mobilenetv3.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/mobilenetv3.py new file mode 100644 index 00000000000..7c09bb3a160 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/mobilenetv3.py @@ -0,0 +1,366 @@ +""" MobileNet-V3 + +A PyTorch impl of MobileNet-V3, compatible with TF weights from official impl. + +Paper: Searching for MobileNetV3 - https://arxiv.org/abs/1905.02244 + +Hacked together by / Copyright 2020 Ross Wightman +""" +import torch.nn as nn +import torch.nn.functional as F + +from .activations import get_act_fn, get_act_layer, HardSwish +from .config import layer_config_kwargs +from .conv2d_layers import select_conv2d +from .helpers import load_pretrained +from .efficientnet_builder import (BN_EPS_TF_DEFAULT, EfficientNetBuilder, decode_arch_def, + initialize_weight_default, initialize_weight_goog, + resolve_act_layer, resolve_bn_args, round_channels) + +__all__ = ['mobilenetv3_rw', 'mobilenetv3_large_075', 'mobilenetv3_large_100', 'mobilenetv3_large_minimal_100', + 'mobilenetv3_small_075', 'mobilenetv3_small_100', 'mobilenetv3_small_minimal_100', + 'tf_mobilenetv3_large_075', 'tf_mobilenetv3_large_100', 'tf_mobilenetv3_large_minimal_100', + 'tf_mobilenetv3_small_075', 'tf_mobilenetv3_small_100', 'tf_mobilenetv3_small_minimal_100'] + +model_urls = { + 'mobilenetv3_rw': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/mobilenetv3_100-35495452.pth', + 'mobilenetv3_large_075': None, + 'mobilenetv3_large_100': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/mobilenetv3_large_100_ra-f55367f5.pth', + 'mobilenetv3_large_minimal_100': None, + 'mobilenetv3_small_075': None, + 'mobilenetv3_small_100': None, + 'mobilenetv3_small_minimal_100': None, + 'tf_mobilenetv3_large_075': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_mobilenetv3_large_075-150ee8b0.pth', + 'tf_mobilenetv3_large_100': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_mobilenetv3_large_100-427764d5.pth', + 'tf_mobilenetv3_large_minimal_100': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_mobilenetv3_large_minimal_100-8596ae28.pth', + 'tf_mobilenetv3_small_075': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_mobilenetv3_small_075-da427f52.pth', + 'tf_mobilenetv3_small_100': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_mobilenetv3_small_100-37f49e2b.pth', + 'tf_mobilenetv3_small_minimal_100': + 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/tf_mobilenetv3_small_minimal_100-922a7843.pth', +} + + +class MobileNetV3(nn.Module): + """ MobileNet-V3 + + A this model utilizes the MobileNet-v3 specific 'efficient head', where global pooling is done before the + head convolution without a final batch-norm layer before the classifier. + + Paper: https://arxiv.org/abs/1905.02244 + """ + + def __init__(self, block_args, num_classes=1000, in_chans=3, stem_size=16, num_features=1280, head_bias=True, + channel_multiplier=1.0, pad_type='', act_layer=HardSwish, drop_rate=0., drop_connect_rate=0., + se_kwargs=None, norm_layer=nn.BatchNorm2d, norm_kwargs=None, weight_init='goog'): + super(MobileNetV3, self).__init__() + self.drop_rate = drop_rate + + stem_size = round_channels(stem_size, channel_multiplier) + self.conv_stem = select_conv2d(in_chans, stem_size, 3, stride=2, padding=pad_type) + self.bn1 = nn.BatchNorm2d(stem_size, **norm_kwargs) + self.act1 = act_layer(inplace=True) + in_chs = stem_size + + builder = EfficientNetBuilder( + channel_multiplier, pad_type=pad_type, act_layer=act_layer, se_kwargs=se_kwargs, + norm_layer=norm_layer, norm_kwargs=norm_kwargs, drop_connect_rate=drop_connect_rate) + self.blocks = nn.Sequential(*builder(in_chs, block_args)) + in_chs = builder.in_chs + + self.global_pool = nn.AdaptiveAvgPool2d(1) + self.conv_head = select_conv2d(in_chs, num_features, 1, padding=pad_type, bias=head_bias) + self.act2 = act_layer(inplace=True) + self.classifier = nn.Linear(num_features, num_classes) + + for m in self.modules(): + if weight_init == 'goog': + initialize_weight_goog(m) + else: + initialize_weight_default(m) + + def as_sequential(self): + layers = [self.conv_stem, self.bn1, self.act1] + layers.extend(self.blocks) + layers.extend([ + self.global_pool, self.conv_head, self.act2, + nn.Flatten(), nn.Dropout(self.drop_rate), self.classifier]) + return nn.Sequential(*layers) + + def features(self, x): + x = self.conv_stem(x) + x = self.bn1(x) + x = self.act1(x) + x = self.blocks(x) + x = self.global_pool(x) + x = self.conv_head(x) + x = self.act2(x) + return x + + def forward(self, x): + x = self.features(x) + x = x.flatten(1) + if self.drop_rate > 0.: + x = F.dropout(x, p=self.drop_rate, training=self.training) + return self.classifier(x) + + +def _create_model(model_kwargs, variant, pretrained=False): + as_sequential = model_kwargs.pop('as_sequential', False) + model = MobileNetV3(**model_kwargs) + if pretrained and model_urls[variant]: + load_pretrained(model, model_urls[variant]) + if as_sequential: + model = model.as_sequential() + return model + + +def _gen_mobilenet_v3_rw(variant, channel_multiplier=1.0, pretrained=False, **kwargs): + """Creates a MobileNet-V3 model (RW variant). + + Paper: https://arxiv.org/abs/1905.02244 + + This was my first attempt at reproducing the MobileNet-V3 from paper alone. It came close to the + eventual Tensorflow reference impl but has a few differences: + 1. This model has no bias on the head convolution + 2. This model forces no residual (noskip) on the first DWS block, this is different than MnasNet + 3. This model always uses ReLU for the SE activation layer, other models in the family inherit their act layer + from their parent block + 4. This model does not enforce divisible by 8 limitation on the SE reduction channel count + + Overall the changes are fairly minor and result in a very small parameter count difference and no + top-1/5 + + Args: + channel_multiplier: multiplier to number of channels per layer. + """ + arch_def = [ + # stage 0, 112x112 in + ['ds_r1_k3_s1_e1_c16_nre_noskip'], # relu + # stage 1, 112x112 in + ['ir_r1_k3_s2_e4_c24_nre', 'ir_r1_k3_s1_e3_c24_nre'], # relu + # stage 2, 56x56 in + ['ir_r3_k5_s2_e3_c40_se0.25_nre'], # relu + # stage 3, 28x28 in + ['ir_r1_k3_s2_e6_c80', 'ir_r1_k3_s1_e2.5_c80', 'ir_r2_k3_s1_e2.3_c80'], # hard-swish + # stage 4, 14x14in + ['ir_r2_k3_s1_e6_c112_se0.25'], # hard-swish + # stage 5, 14x14in + ['ir_r3_k5_s2_e6_c160_se0.25'], # hard-swish + # stage 6, 7x7 in + ['cn_r1_k1_s1_c960'], # hard-swish + ] + with layer_config_kwargs(kwargs): + model_kwargs = dict( + block_args=decode_arch_def(arch_def), + head_bias=False, # one of my mistakes + channel_multiplier=channel_multiplier, + act_layer=resolve_act_layer(kwargs, 'hard_swish'), + se_kwargs=dict(gate_fn=get_act_fn('hard_sigmoid'), reduce_mid=True), + norm_kwargs=resolve_bn_args(kwargs), + **kwargs, + ) + model = _create_model(model_kwargs, variant, pretrained) + return model + + +def _gen_mobilenet_v3(variant, channel_multiplier=1.0, pretrained=False, **kwargs): + """Creates a MobileNet-V3 large/small/minimal models. + + Ref impl: https://github.com/tensorflow/models/blob/master/research/slim/nets/mobilenet/mobilenet_v3.py + Paper: https://arxiv.org/abs/1905.02244 + + Args: + channel_multiplier: multiplier to number of channels per layer. + """ + if 'small' in variant: + num_features = 1024 + if 'minimal' in variant: + act_layer = 'relu' + arch_def = [ + # stage 0, 112x112 in + ['ds_r1_k3_s2_e1_c16'], + # stage 1, 56x56 in + ['ir_r1_k3_s2_e4.5_c24', 'ir_r1_k3_s1_e3.67_c24'], + # stage 2, 28x28 in + ['ir_r1_k3_s2_e4_c40', 'ir_r2_k3_s1_e6_c40'], + # stage 3, 14x14 in + ['ir_r2_k3_s1_e3_c48'], + # stage 4, 14x14in + ['ir_r3_k3_s2_e6_c96'], + # stage 6, 7x7 in + ['cn_r1_k1_s1_c576'], + ] + else: + act_layer = 'hard_swish' + arch_def = [ + # stage 0, 112x112 in + ['ds_r1_k3_s2_e1_c16_se0.25_nre'], # relu + # stage 1, 56x56 in + ['ir_r1_k3_s2_e4.5_c24_nre', 'ir_r1_k3_s1_e3.67_c24_nre'], # relu + # stage 2, 28x28 in + ['ir_r1_k5_s2_e4_c40_se0.25', 'ir_r2_k5_s1_e6_c40_se0.25'], # hard-swish + # stage 3, 14x14 in + ['ir_r2_k5_s1_e3_c48_se0.25'], # hard-swish + # stage 4, 14x14in + ['ir_r3_k5_s2_e6_c96_se0.25'], # hard-swish + # stage 6, 7x7 in + ['cn_r1_k1_s1_c576'], # hard-swish + ] + else: + num_features = 1280 + if 'minimal' in variant: + act_layer = 'relu' + arch_def = [ + # stage 0, 112x112 in + ['ds_r1_k3_s1_e1_c16'], + # stage 1, 112x112 in + ['ir_r1_k3_s2_e4_c24', 'ir_r1_k3_s1_e3_c24'], + # stage 2, 56x56 in + ['ir_r3_k3_s2_e3_c40'], + # stage 3, 28x28 in + ['ir_r1_k3_s2_e6_c80', 'ir_r1_k3_s1_e2.5_c80', 'ir_r2_k3_s1_e2.3_c80'], + # stage 4, 14x14in + ['ir_r2_k3_s1_e6_c112'], + # stage 5, 14x14in + ['ir_r3_k3_s2_e6_c160'], + # stage 6, 7x7 in + ['cn_r1_k1_s1_c960'], + ] + else: + act_layer = 'hard_swish' + arch_def = [ + # stage 0, 112x112 in + ['ds_r1_k3_s1_e1_c16_nre'], # relu + # stage 1, 112x112 in + ['ir_r1_k3_s2_e4_c24_nre', 'ir_r1_k3_s1_e3_c24_nre'], # relu + # stage 2, 56x56 in + ['ir_r3_k5_s2_e3_c40_se0.25_nre'], # relu + # stage 3, 28x28 in + ['ir_r1_k3_s2_e6_c80', 'ir_r1_k3_s1_e2.5_c80', 'ir_r2_k3_s1_e2.3_c80'], # hard-swish + # stage 4, 14x14in + ['ir_r2_k3_s1_e6_c112_se0.25'], # hard-swish + # stage 5, 14x14in + ['ir_r3_k5_s2_e6_c160_se0.25'], # hard-swish + # stage 6, 7x7 in + ['cn_r1_k1_s1_c960'], # hard-swish + ] + with layer_config_kwargs(kwargs): + model_kwargs = dict( + block_args=decode_arch_def(arch_def), + num_features=num_features, + stem_size=16, + channel_multiplier=channel_multiplier, + act_layer=resolve_act_layer(kwargs, act_layer), + se_kwargs=dict( + act_layer=get_act_layer('relu'), gate_fn=get_act_fn('hard_sigmoid'), reduce_mid=True, divisor=8), + norm_kwargs=resolve_bn_args(kwargs), + **kwargs, + ) + model = _create_model(model_kwargs, variant, pretrained) + return model + + +def mobilenetv3_rw(pretrained=False, **kwargs): + """ MobileNet-V3 RW + Attn: See note in gen function for this variant. + """ + # NOTE for train set drop_rate=0.2 + if pretrained: + # pretrained model trained with non-default BN epsilon + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + model = _gen_mobilenet_v3_rw('mobilenetv3_rw', 1.0, pretrained=pretrained, **kwargs) + return model + + +def mobilenetv3_large_075(pretrained=False, **kwargs): + """ MobileNet V3 Large 0.75""" + # NOTE for train set drop_rate=0.2 + model = _gen_mobilenet_v3('mobilenetv3_large_075', 0.75, pretrained=pretrained, **kwargs) + return model + + +def mobilenetv3_large_100(pretrained=False, **kwargs): + """ MobileNet V3 Large 1.0 """ + # NOTE for train set drop_rate=0.2 + model = _gen_mobilenet_v3('mobilenetv3_large_100', 1.0, pretrained=pretrained, **kwargs) + return model + + +def mobilenetv3_large_minimal_100(pretrained=False, **kwargs): + """ MobileNet V3 Large (Minimalistic) 1.0 """ + # NOTE for train set drop_rate=0.2 + model = _gen_mobilenet_v3('mobilenetv3_large_minimal_100', 1.0, pretrained=pretrained, **kwargs) + return model + + +def mobilenetv3_small_075(pretrained=False, **kwargs): + """ MobileNet V3 Small 0.75 """ + model = _gen_mobilenet_v3('mobilenetv3_small_075', 0.75, pretrained=pretrained, **kwargs) + return model + + +def mobilenetv3_small_100(pretrained=False, **kwargs): + """ MobileNet V3 Small 1.0 """ + model = _gen_mobilenet_v3('mobilenetv3_small_100', 1.0, pretrained=pretrained, **kwargs) + return model + + +def mobilenetv3_small_minimal_100(pretrained=False, **kwargs): + """ MobileNet V3 Small (Minimalistic) 1.0 """ + model = _gen_mobilenet_v3('mobilenetv3_small_minimal_100', 1.0, pretrained=pretrained, **kwargs) + return model + + +def tf_mobilenetv3_large_075(pretrained=False, **kwargs): + """ MobileNet V3 Large 0.75. Tensorflow compat variant. """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_mobilenet_v3('tf_mobilenetv3_large_075', 0.75, pretrained=pretrained, **kwargs) + return model + + +def tf_mobilenetv3_large_100(pretrained=False, **kwargs): + """ MobileNet V3 Large 1.0. Tensorflow compat variant. """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_mobilenet_v3('tf_mobilenetv3_large_100', 1.0, pretrained=pretrained, **kwargs) + return model + + +def tf_mobilenetv3_large_minimal_100(pretrained=False, **kwargs): + """ MobileNet V3 Large Minimalistic 1.0. Tensorflow compat variant. """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_mobilenet_v3('tf_mobilenetv3_large_minimal_100', 1.0, pretrained=pretrained, **kwargs) + return model + + +def tf_mobilenetv3_small_075(pretrained=False, **kwargs): + """ MobileNet V3 Small 0.75. Tensorflow compat variant. """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_mobilenet_v3('tf_mobilenetv3_small_075', 0.75, pretrained=pretrained, **kwargs) + return model + + +def tf_mobilenetv3_small_100(pretrained=False, **kwargs): + """ MobileNet V3 Small 1.0. Tensorflow compat variant.""" + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_mobilenet_v3('tf_mobilenetv3_small_100', 1.0, pretrained=pretrained, **kwargs) + return model + + +def tf_mobilenetv3_small_minimal_100(pretrained=False, **kwargs): + """ MobileNet V3 Small Minimalistic 1.0. Tensorflow compat variant. """ + kwargs['bn_eps'] = BN_EPS_TF_DEFAULT + kwargs['pad_type'] = 'same' + model = _gen_mobilenet_v3('tf_mobilenetv3_small_minimal_100', 1.0, pretrained=pretrained, **kwargs) + return model diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/model_factory.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/model_factory.py new file mode 100644 index 00000000000..4d46ea8baed --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/model_factory.py @@ -0,0 +1,27 @@ +from .config import set_layer_config +from .helpers import load_checkpoint + +from .gen_efficientnet import * +from .mobilenetv3 import * + + +def create_model( + model_name='mnasnet_100', + pretrained=None, + num_classes=1000, + in_chans=3, + checkpoint_path='', + **kwargs): + + model_kwargs = dict(num_classes=num_classes, in_chans=in_chans, pretrained=pretrained, **kwargs) + + if model_name in globals(): + create_fn = globals()[model_name] + model = create_fn(**model_kwargs) + else: + raise RuntimeError('Unknown model (%s)' % model_name) + + if checkpoint_path and not pretrained: + load_checkpoint(model, checkpoint_path) + + return model diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/version.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/version.py new file mode 100644 index 00000000000..a6221b3de7b --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/geffnet/version.py @@ -0,0 +1 @@ +__version__ = '1.0.2' diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/hubconf.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/hubconf.py new file mode 100644 index 00000000000..45b17b99bbe --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/hubconf.py @@ -0,0 +1,84 @@ +dependencies = ['torch', 'math'] + +from geffnet import efficientnet_b0 +from geffnet import efficientnet_b1 +from geffnet import efficientnet_b2 +from geffnet import efficientnet_b3 + +from geffnet import efficientnet_es + +from geffnet import efficientnet_lite0 + +from geffnet import mixnet_s +from geffnet import mixnet_m +from geffnet import mixnet_l +from geffnet import mixnet_xl + +from geffnet import mobilenetv2_100 +from geffnet import mobilenetv2_110d +from geffnet import mobilenetv2_120d +from geffnet import mobilenetv2_140 + +from geffnet import mobilenetv3_large_100 +from geffnet import mobilenetv3_rw +from geffnet import mnasnet_a1 +from geffnet import mnasnet_b1 +from geffnet import fbnetc_100 +from geffnet import spnasnet_100 + +from geffnet import tf_efficientnet_b0 +from geffnet import tf_efficientnet_b1 +from geffnet import tf_efficientnet_b2 +from geffnet import tf_efficientnet_b3 +from geffnet import tf_efficientnet_b4 +from geffnet import tf_efficientnet_b5 +from geffnet import tf_efficientnet_b6 +from geffnet import tf_efficientnet_b7 +from geffnet import tf_efficientnet_b8 + +from geffnet import tf_efficientnet_b0_ap +from geffnet import tf_efficientnet_b1_ap +from geffnet import tf_efficientnet_b2_ap +from geffnet import tf_efficientnet_b3_ap +from geffnet import tf_efficientnet_b4_ap +from geffnet import tf_efficientnet_b5_ap +from geffnet import tf_efficientnet_b6_ap +from geffnet import tf_efficientnet_b7_ap +from geffnet import tf_efficientnet_b8_ap + +from geffnet import tf_efficientnet_b0_ns +from geffnet import tf_efficientnet_b1_ns +from geffnet import tf_efficientnet_b2_ns +from geffnet import tf_efficientnet_b3_ns +from geffnet import tf_efficientnet_b4_ns +from geffnet import tf_efficientnet_b5_ns +from geffnet import tf_efficientnet_b6_ns +from geffnet import tf_efficientnet_b7_ns +from geffnet import tf_efficientnet_l2_ns_475 +from geffnet import tf_efficientnet_l2_ns + +from geffnet import tf_efficientnet_es +from geffnet import tf_efficientnet_em +from geffnet import tf_efficientnet_el + +from geffnet import tf_efficientnet_cc_b0_4e +from geffnet import tf_efficientnet_cc_b0_8e +from geffnet import tf_efficientnet_cc_b1_8e + +from geffnet import tf_efficientnet_lite0 +from geffnet import tf_efficientnet_lite1 +from geffnet import tf_efficientnet_lite2 +from geffnet import tf_efficientnet_lite3 +from geffnet import tf_efficientnet_lite4 + +from geffnet import tf_mixnet_s +from geffnet import tf_mixnet_m +from geffnet import tf_mixnet_l + +from geffnet import tf_mobilenetv3_large_075 +from geffnet import tf_mobilenetv3_large_100 +from geffnet import tf_mobilenetv3_large_minimal_100 +from geffnet import tf_mobilenetv3_small_075 +from geffnet import tf_mobilenetv3_small_100 +from geffnet import tf_mobilenetv3_small_minimal_100 + diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/onnx_export.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/onnx_export.py new file mode 100644 index 00000000000..7a5162ce214 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/onnx_export.py @@ -0,0 +1,120 @@ +""" ONNX export script + +Export PyTorch models as ONNX graphs. + +This export script originally started as an adaptation of code snippets found at +https://pytorch.org/tutorials/advanced/super_resolution_with_onnxruntime.html + +The default parameters work with PyTorch 1.6 and ONNX 1.7 and produce an optimal ONNX graph +for hosting in the ONNX runtime (see onnx_validate.py). To export an ONNX model compatible +with caffe2 (see caffe2_benchmark.py and caffe2_validate.py), the --keep-init and --aten-fallback +flags are currently required. + +Older versions of PyTorch/ONNX (tested PyTorch 1.4, ONNX 1.5) do not need extra flags for +caffe2 compatibility, but they produce a model that isn't as fast running on ONNX runtime. + +Most new release of PyTorch and ONNX cause some sort of breakage in the export / usage of ONNX models. +Please do your research and search ONNX and PyTorch issue tracker before asking me. Thanks. + +Copyright 2020 Ross Wightman +""" +import argparse +import torch +import numpy as np + +import onnx +import geffnet + +parser = argparse.ArgumentParser(description='PyTorch ImageNet Validation') +parser.add_argument('output', metavar='ONNX_FILE', + help='output model filename') +parser.add_argument('--model', '-m', metavar='MODEL', default='mobilenetv3_large_100', + help='model architecture (default: mobilenetv3_large_100)') +parser.add_argument('--opset', type=int, default=10, + help='ONNX opset to use (default: 10)') +parser.add_argument('--keep-init', action='store_true', default=False, + help='Keep initializers as input. Needed for Caffe2 compatible export in newer PyTorch/ONNX.') +parser.add_argument('--aten-fallback', action='store_true', default=False, + help='Fallback to ATEN ops. Helps fix AdaptiveAvgPool issue with Caffe2 in newer PyTorch/ONNX.') +parser.add_argument('--dynamic-size', action='store_true', default=False, + help='Export model width dynamic width/height. Not recommended for "tf" models with SAME padding.') +parser.add_argument('-b', '--batch-size', default=1, type=int, + metavar='N', help='mini-batch size (default: 1)') +parser.add_argument('--img-size', default=None, type=int, + metavar='N', help='Input image dimension, uses model default if empty') +parser.add_argument('--mean', type=float, nargs='+', default=None, metavar='MEAN', + help='Override mean pixel value of dataset') +parser.add_argument('--std', type=float, nargs='+', default=None, metavar='STD', + help='Override std deviation of of dataset') +parser.add_argument('--num-classes', type=int, default=1000, + help='Number classes in dataset') +parser.add_argument('--checkpoint', default='', type=str, metavar='PATH', + help='path to checkpoint (default: none)') + + +def main(): + args = parser.parse_args() + + args.pretrained = True + if args.checkpoint: + args.pretrained = False + + print("==> Creating PyTorch {} model".format(args.model)) + # NOTE exportable=True flag disables autofn/jit scripted activations and uses Conv2dSameExport layers + # for models using SAME padding + model = geffnet.create_model( + args.model, + num_classes=args.num_classes, + in_chans=3, + pretrained=args.pretrained, + checkpoint_path=args.checkpoint, + exportable=True) + + model.eval() + + example_input = torch.randn((args.batch_size, 3, args.img_size or 224, args.img_size or 224), requires_grad=True) + + # Run model once before export trace, sets padding for models with Conv2dSameExport. This means + # that the padding for models with Conv2dSameExport (most models with tf_ prefix) is fixed for + # the input img_size specified in this script. + # Opset >= 11 should allow for dynamic padding, however I cannot get it to work due to + # issues in the tracing of the dynamic padding or errors attempting to export the model after jit + # scripting it (an approach that should work). Perhaps in a future PyTorch or ONNX versions... + model(example_input) + + print("==> Exporting model to ONNX format at '{}'".format(args.output)) + input_names = ["input0"] + output_names = ["output0"] + dynamic_axes = {'input0': {0: 'batch'}, 'output0': {0: 'batch'}} + if args.dynamic_size: + dynamic_axes['input0'][2] = 'height' + dynamic_axes['input0'][3] = 'width' + if args.aten_fallback: + export_type = torch.onnx.OperatorExportTypes.ONNX_ATEN_FALLBACK + else: + export_type = torch.onnx.OperatorExportTypes.ONNX + + torch_out = torch.onnx._export( + model, example_input, args.output, export_params=True, verbose=True, input_names=input_names, + output_names=output_names, keep_initializers_as_inputs=args.keep_init, dynamic_axes=dynamic_axes, + opset_version=args.opset, operator_export_type=export_type) + + print("==> Loading and checking exported model from '{}'".format(args.output)) + onnx_model = onnx.load(args.output) + onnx.checker.check_model(onnx_model) # assuming throw on error + print("==> Passed") + + if args.keep_init and args.aten_fallback: + import caffe2.python.onnx.backend as onnx_caffe2 + # Caffe2 loading only works properly in newer PyTorch/ONNX combos when + # keep_initializers_as_inputs and aten_fallback are set to True. + print("==> Loading model into Caffe2 backend and comparing forward pass.".format(args.output)) + caffe2_backend = onnx_caffe2.prepare(onnx_model) + B = {onnx_model.graph.input[0].name: x.data.numpy()} + c2_out = caffe2_backend.run(B)[0] + np.testing.assert_almost_equal(torch_out.data.numpy(), c2_out, decimal=5) + print("==> Passed") + + +if __name__ == '__main__': + main() diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/onnx_optimize.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/onnx_optimize.py new file mode 100644 index 00000000000..ee20bbf9f0f --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/onnx_optimize.py @@ -0,0 +1,84 @@ +""" ONNX optimization script + +Run ONNX models through the optimizer to prune unneeded nodes, fuse batchnorm layers into conv, etc. + +NOTE: This isn't working consistently in recent PyTorch/ONNX combos (ie PyTorch 1.6 and ONNX 1.7), +it seems time to switch to using the onnxruntime online optimizer (can also be saved for offline). + +Copyright 2020 Ross Wightman +""" +import argparse +import warnings + +import onnx +from onnx import optimizer + + +parser = argparse.ArgumentParser(description="Optimize ONNX model") + +parser.add_argument("model", help="The ONNX model") +parser.add_argument("--output", required=True, help="The optimized model output filename") + + +def traverse_graph(graph, prefix=''): + content = [] + indent = prefix + ' ' + graphs = [] + num_nodes = 0 + for node in graph.node: + pn, gs = onnx.helper.printable_node(node, indent, subgraphs=True) + assert isinstance(gs, list) + content.append(pn) + graphs.extend(gs) + num_nodes += 1 + for g in graphs: + g_count, g_str = traverse_graph(g) + content.append('\n' + g_str) + num_nodes += g_count + return num_nodes, '\n'.join(content) + + +def main(): + args = parser.parse_args() + onnx_model = onnx.load(args.model) + num_original_nodes, original_graph_str = traverse_graph(onnx_model.graph) + + # Optimizer passes to perform + passes = [ + #'eliminate_deadend', + 'eliminate_identity', + 'eliminate_nop_dropout', + 'eliminate_nop_pad', + 'eliminate_nop_transpose', + 'eliminate_unused_initializer', + 'extract_constant_to_initializer', + 'fuse_add_bias_into_conv', + 'fuse_bn_into_conv', + 'fuse_consecutive_concats', + 'fuse_consecutive_reduce_unsqueeze', + 'fuse_consecutive_squeezes', + 'fuse_consecutive_transposes', + #'fuse_matmul_add_bias_into_gemm', + 'fuse_pad_into_conv', + #'fuse_transpose_into_gemm', + #'lift_lexical_references', + ] + + # Apply the optimization on the original serialized model + # WARNING I've had issues with optimizer in recent versions of PyTorch / ONNX causing + # 'duplicate definition of name' errors, see: https://github.com/onnx/onnx/issues/2401 + # It may be better to rely on onnxruntime optimizations, see onnx_validate.py script. + warnings.warn("I've had issues with optimizer in recent versions of PyTorch / ONNX." + "Try onnxruntime optimization if this doesn't work.") + optimized_model = optimizer.optimize(onnx_model, passes) + + num_optimized_nodes, optimzied_graph_str = traverse_graph(optimized_model.graph) + print('==> The model after optimization:\n{}\n'.format(optimzied_graph_str)) + print('==> The optimized model has {} nodes, the original had {}.'.format(num_optimized_nodes, num_original_nodes)) + + # Save the ONNX model + onnx.save(optimized_model, args.output) + + +if __name__ == "__main__": + main() diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/onnx_to_caffe.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/onnx_to_caffe.py new file mode 100644 index 00000000000..44399aafaba --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/onnx_to_caffe.py @@ -0,0 +1,27 @@ +import argparse + +import onnx +from caffe2.python.onnx.backend import Caffe2Backend + + +parser = argparse.ArgumentParser(description="Convert ONNX to Caffe2") + +parser.add_argument("model", help="The ONNX model") +parser.add_argument("--c2-prefix", required=True, + help="The output file prefix for the caffe2 model init and predict file. ") + + +def main(): + args = parser.parse_args() + onnx_model = onnx.load(args.model) + caffe2_init, caffe2_predict = Caffe2Backend.onnx_graph_to_caffe2_net(onnx_model) + caffe2_init_str = caffe2_init.SerializeToString() + with open(args.c2_prefix + '.init.pb', "wb") as f: + f.write(caffe2_init_str) + caffe2_predict_str = caffe2_predict.SerializeToString() + with open(args.c2_prefix + '.predict.pb', "wb") as f: + f.write(caffe2_predict_str) + + +if __name__ == "__main__": + main() diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/onnx_validate.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/onnx_validate.py new file mode 100644 index 00000000000..ab3e4fb141b --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/onnx_validate.py @@ -0,0 +1,112 @@ +""" ONNX-runtime validation script + +This script was created to verify accuracy and performance of exported ONNX +models running with the onnxruntime. It utilizes the PyTorch dataloader/processing +pipeline for a fair comparison against the originals. + +Copyright 2020 Ross Wightman +""" +import argparse +import numpy as np +import onnxruntime +from data import create_loader, resolve_data_config, Dataset +from utils import AverageMeter +import time + +parser = argparse.ArgumentParser(description='Caffe2 ImageNet Validation') +parser.add_argument('data', metavar='DIR', + help='path to dataset') +parser.add_argument('--onnx-input', default='', type=str, metavar='PATH', + help='path to onnx model/weights file') +parser.add_argument('--onnx-output-opt', default='', type=str, metavar='PATH', + help='path to output optimized onnx graph') +parser.add_argument('--profile', action='store_true', default=False, + help='Enable profiler output.') +parser.add_argument('-j', '--workers', default=2, type=int, metavar='N', + help='number of data loading workers (default: 2)') +parser.add_argument('-b', '--batch-size', default=256, type=int, + metavar='N', help='mini-batch size (default: 256)') +parser.add_argument('--img-size', default=None, type=int, + metavar='N', help='Input image dimension, uses model default if empty') +parser.add_argument('--mean', type=float, nargs='+', default=None, metavar='MEAN', + help='Override mean pixel value of dataset') +parser.add_argument('--std', type=float, nargs='+', default=None, metavar='STD', + help='Override std deviation of of dataset') +parser.add_argument('--crop-pct', type=float, default=None, metavar='PCT', + help='Override default crop pct of 0.875') +parser.add_argument('--interpolation', default='', type=str, metavar='NAME', + help='Image resize interpolation type (overrides model)') +parser.add_argument('--tf-preprocessing', dest='tf_preprocessing', action='store_true', + help='use tensorflow mnasnet preporcessing') +parser.add_argument('--print-freq', '-p', default=10, type=int, + metavar='N', help='print frequency (default: 10)') + + +def main(): + args = parser.parse_args() + args.gpu_id = 0 + + # Set graph optimization level + sess_options = onnxruntime.SessionOptions() + sess_options.graph_optimization_level = onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL + if args.profile: + sess_options.enable_profiling = True + if args.onnx_output_opt: + sess_options.optimized_model_filepath = args.onnx_output_opt + + session = onnxruntime.InferenceSession(args.onnx_input, sess_options) + + data_config = resolve_data_config(None, args) + loader = create_loader( + Dataset(args.data, load_bytes=args.tf_preprocessing), + input_size=data_config['input_size'], + batch_size=args.batch_size, + use_prefetcher=False, + interpolation=data_config['interpolation'], + mean=data_config['mean'], + std=data_config['std'], + num_workers=args.workers, + crop_pct=data_config['crop_pct'], + tensorflow_preprocessing=args.tf_preprocessing) + + input_name = session.get_inputs()[0].name + + batch_time = AverageMeter() + top1 = AverageMeter() + top5 = AverageMeter() + end = time.time() + for i, (input, target) in enumerate(loader): + # run the net and return prediction + output = session.run([], {input_name: input.data.numpy()}) + output = output[0] + + # measure accuracy and record loss + prec1, prec5 = accuracy_np(output, target.numpy()) + top1.update(prec1.item(), input.size(0)) + top5.update(prec5.item(), input.size(0)) + + # measure elapsed time + batch_time.update(time.time() - end) + end = time.time() + + if i % args.print_freq == 0: + print('Test: [{0}/{1}]\t' + 'Time {batch_time.val:.3f} ({batch_time.avg:.3f}, {rate_avg:.3f}/s, {ms_avg:.3f} ms/sample) \t' + 'Prec@1 {top1.val:.3f} ({top1.avg:.3f})\t' + 'Prec@5 {top5.val:.3f} ({top5.avg:.3f})'.format( + i, len(loader), batch_time=batch_time, rate_avg=input.size(0) / batch_time.avg, + ms_avg=100 * batch_time.avg / input.size(0), top1=top1, top5=top5)) + + print(' * Prec@1 {top1.avg:.3f} ({top1a:.3f}) Prec@5 {top5.avg:.3f} ({top5a:.3f})'.format( + top1=top1, top1a=100-top1.avg, top5=top5, top5a=100.-top5.avg)) + + +def accuracy_np(output, target): + max_indices = np.argsort(output, axis=1)[:, ::-1] + top5 = 100 * np.equal(max_indices[:, :5], target[:, np.newaxis]).sum(axis=1).mean() + top1 = 100 * np.equal(max_indices[:, 0], target).mean() + return top1, top5 + + +if __name__ == '__main__': + main() diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/requirements.txt b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/requirements.txt new file mode 100644 index 00000000000..ac3ffc13bae --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/requirements.txt @@ -0,0 +1,2 @@ +torch>=1.2.0 +torchvision>=0.4.0 diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/setup.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/setup.py new file mode 100644 index 00000000000..023e4c30f98 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/setup.py @@ -0,0 +1,47 @@ +""" Setup +""" +from setuptools import setup, find_packages +from codecs import open +from os import path + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the README file +with open(path.join(here, 'README.md'), encoding='utf-8') as f: + long_description = f.read() + +exec(open('geffnet/version.py').read()) +setup( + name='geffnet', + version=__version__, + description='(Generic) EfficientNets for PyTorch', + long_description=long_description, + long_description_content_type='text/markdown', + url='https://github.com/rwightman/gen-efficientnet-pytorch', + author='Ross Wightman', + author_email='hello@rwightman.com', + classifiers=[ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Education', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Topic :: Scientific/Engineering', + 'Topic :: Scientific/Engineering :: Artificial Intelligence', + 'Topic :: Software Development', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + + # Note that this is a string of words separated by whitespace, not a list. + keywords='pytorch pretrained models efficientnet mixnet mobilenetv3 mnasnet', + packages=find_packages(exclude=['data']), + install_requires=['torch >= 1.4', 'torchvision'], + python_requires='>=3.6', +) diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/utils.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/utils.py new file mode 100644 index 00000000000..d327e8bd812 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/utils.py @@ -0,0 +1,52 @@ +import os + + +class AverageMeter: + """Computes and stores the average and current value""" + def __init__(self): + self.reset() + + def reset(self): + self.val = 0 + self.avg = 0 + self.sum = 0 + self.count = 0 + + def update(self, val, n=1): + self.val = val + self.sum += val * n + self.count += n + self.avg = self.sum / self.count + + +def accuracy(output, target, topk=(1,)): + """Computes the precision@k for the specified values of k""" + maxk = max(topk) + batch_size = target.size(0) + + _, pred = output.topk(maxk, 1, True, True) + pred = pred.t() + correct = pred.eq(target.view(1, -1).expand_as(pred)) + + res = [] + for k in topk: + correct_k = correct[:k].reshape(-1).float().sum(0) + res.append(correct_k.mul_(100.0 / batch_size)) + return res + + +def get_outdir(path, *paths, inc=False): + outdir = os.path.join(path, *paths) + if not os.path.exists(outdir): + os.makedirs(outdir) + elif inc: + count = 1 + outdir_inc = outdir + '-' + str(count) + while os.path.exists(outdir_inc): + count = count + 1 + outdir_inc = outdir + '-' + str(count) + assert count < 100 + outdir = outdir_inc + os.makedirs(outdir) + return outdir + diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/validate.py b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/validate.py new file mode 100644 index 00000000000..5fd44fbb316 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/efficientnet_repo/validate.py @@ -0,0 +1,166 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import argparse +import time +import torch +import torch.nn as nn +import torch.nn.parallel +from contextlib import suppress + +import geffnet +from data import Dataset, create_loader, resolve_data_config +from utils import accuracy, AverageMeter + +has_native_amp = False +try: + if getattr(torch.cuda.amp, 'autocast') is not None: + has_native_amp = True +except AttributeError: + pass + +torch.backends.cudnn.benchmark = True + +parser = argparse.ArgumentParser(description='PyTorch ImageNet Validation') +parser.add_argument('data', metavar='DIR', + help='path to dataset') +parser.add_argument('--model', '-m', metavar='MODEL', default='spnasnet1_00', + help='model architecture (default: dpn92)') +parser.add_argument('-j', '--workers', default=4, type=int, metavar='N', + help='number of data loading workers (default: 2)') +parser.add_argument('-b', '--batch-size', default=256, type=int, + metavar='N', help='mini-batch size (default: 256)') +parser.add_argument('--img-size', default=None, type=int, + metavar='N', help='Input image dimension, uses model default if empty') +parser.add_argument('--mean', type=float, nargs='+', default=None, metavar='MEAN', + help='Override mean pixel value of dataset') +parser.add_argument('--std', type=float, nargs='+', default=None, metavar='STD', + help='Override std deviation of of dataset') +parser.add_argument('--crop-pct', type=float, default=None, metavar='PCT', + help='Override default crop pct of 0.875') +parser.add_argument('--interpolation', default='', type=str, metavar='NAME', + help='Image resize interpolation type (overrides model)') +parser.add_argument('--num-classes', type=int, default=1000, + help='Number classes in dataset') +parser.add_argument('--print-freq', '-p', default=10, type=int, + metavar='N', help='print frequency (default: 10)') +parser.add_argument('--checkpoint', default='', type=str, metavar='PATH', + help='path to latest checkpoint (default: none)') +parser.add_argument('--pretrained', dest='pretrained', action='store_true', + help='use pre-trained model') +parser.add_argument('--torchscript', dest='torchscript', action='store_true', + help='convert model torchscript for inference') +parser.add_argument('--num-gpu', type=int, default=1, + help='Number of GPUS to use') +parser.add_argument('--tf-preprocessing', dest='tf_preprocessing', action='store_true', + help='use tensorflow mnasnet preporcessing') +parser.add_argument('--no-cuda', dest='no_cuda', action='store_true', + help='') +parser.add_argument('--channels-last', action='store_true', default=False, + help='Use channels_last memory layout') +parser.add_argument('--amp', action='store_true', default=False, + help='Use native Torch AMP mixed precision.') + + +def main(): + args = parser.parse_args() + + if not args.checkpoint and not args.pretrained: + args.pretrained = True + + amp_autocast = suppress # do nothing + if args.amp: + if not has_native_amp: + print("Native Torch AMP is not available (requires torch >= 1.6), using FP32.") + else: + amp_autocast = torch.cuda.amp.autocast + + # create model + model = geffnet.create_model( + args.model, + num_classes=args.num_classes, + in_chans=3, + pretrained=args.pretrained, + checkpoint_path=args.checkpoint, + scriptable=args.torchscript) + + if args.channels_last: + model = model.to(memory_format=torch.channels_last) + + if args.torchscript: + torch.jit.optimized_execution(True) + model = torch.jit.script(model) + + print('Model %s created, param count: %d' % + (args.model, sum([m.numel() for m in model.parameters()]))) + + data_config = resolve_data_config(model, args) + + criterion = nn.CrossEntropyLoss() + + if not args.no_cuda: + if args.num_gpu > 1: + model = torch.nn.DataParallel(model, device_ids=list(range(args.num_gpu))).cuda() + else: + model = model.cuda() + criterion = criterion.cuda() + + loader = create_loader( + Dataset(args.data, load_bytes=args.tf_preprocessing), + input_size=data_config['input_size'], + batch_size=args.batch_size, + use_prefetcher=not args.no_cuda, + interpolation=data_config['interpolation'], + mean=data_config['mean'], + std=data_config['std'], + num_workers=args.workers, + crop_pct=data_config['crop_pct'], + tensorflow_preprocessing=args.tf_preprocessing) + + batch_time = AverageMeter() + losses = AverageMeter() + top1 = AverageMeter() + top5 = AverageMeter() + + model.eval() + end = time.time() + with torch.no_grad(): + for i, (input, target) in enumerate(loader): + if not args.no_cuda: + target = target.cuda() + input = input.cuda() + if args.channels_last: + input = input.contiguous(memory_format=torch.channels_last) + + # compute output + with amp_autocast(): + output = model(input) + loss = criterion(output, target) + + # measure accuracy and record loss + prec1, prec5 = accuracy(output.data, target, topk=(1, 5)) + losses.update(loss.item(), input.size(0)) + top1.update(prec1.item(), input.size(0)) + top5.update(prec5.item(), input.size(0)) + + # measure elapsed time + batch_time.update(time.time() - end) + end = time.time() + + if i % args.print_freq == 0: + print('Test: [{0}/{1}]\t' + 'Time {batch_time.val:.3f} ({batch_time.avg:.3f}, {rate_avg:.3f}/s) \t' + 'Loss {loss.val:.4f} ({loss.avg:.4f})\t' + 'Prec@1 {top1.val:.3f} ({top1.avg:.3f})\t' + 'Prec@5 {top5.val:.3f} ({top5.avg:.3f})'.format( + i, len(loader), batch_time=batch_time, + rate_avg=input.size(0) / batch_time.avg, + loss=losses, top1=top1, top5=top5)) + + print(' * Prec@1 {top1.avg:.3f} ({top1a:.3f}) Prec@5 {top5.avg:.3f} ({top5a:.3f})'.format( + top1=top1, top1a=100-top1.avg, top5=top5, top5a=100.-top5.avg)) + + +if __name__ == '__main__': + main() diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/encoder.py b/invokeai/backend/image_util/normal_bae/nets/submodules/encoder.py new file mode 100644 index 00000000000..7f7149ca3c0 --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/encoder.py @@ -0,0 +1,34 @@ +import os +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class Encoder(nn.Module): + def __init__(self): + super(Encoder, self).__init__() + + basemodel_name = 'tf_efficientnet_b5_ap' + print('Loading base model ()...'.format(basemodel_name), end='') + repo_path = os.path.join(os.path.dirname(__file__), 'efficientnet_repo') + basemodel = torch.hub.load(repo_path, basemodel_name, pretrained=False, source='local') + print('Done.') + + # Remove last layer + print('Removing last two layers (global_pool & classifier).') + basemodel.global_pool = nn.Identity() + basemodel.classifier = nn.Identity() + + self.original_model = basemodel + + def forward(self, x): + features = [x] + for k, v in self.original_model._modules.items(): + if (k == 'blocks'): + for ki, vi in v._modules.items(): + features.append(vi(features[-1])) + else: + features.append(v(features[-1])) + return features + + diff --git a/invokeai/backend/image_util/normal_bae/nets/submodules/submodules.py b/invokeai/backend/image_util/normal_bae/nets/submodules/submodules.py new file mode 100644 index 00000000000..409733351bd --- /dev/null +++ b/invokeai/backend/image_util/normal_bae/nets/submodules/submodules.py @@ -0,0 +1,140 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + + +######################################################################################################################## + + +# Upsample + BatchNorm +class UpSampleBN(nn.Module): + def __init__(self, skip_input, output_features): + super(UpSampleBN, self).__init__() + + self._net = nn.Sequential(nn.Conv2d(skip_input, output_features, kernel_size=3, stride=1, padding=1), + nn.BatchNorm2d(output_features), + nn.LeakyReLU(), + nn.Conv2d(output_features, output_features, kernel_size=3, stride=1, padding=1), + nn.BatchNorm2d(output_features), + nn.LeakyReLU()) + + def forward(self, x, concat_with): + up_x = F.interpolate(x, size=[concat_with.size(2), concat_with.size(3)], mode='bilinear', align_corners=True) + f = torch.cat([up_x, concat_with], dim=1) + return self._net(f) + + +# Upsample + GroupNorm + Weight Standardization +class UpSampleGN(nn.Module): + def __init__(self, skip_input, output_features): + super(UpSampleGN, self).__init__() + + self._net = nn.Sequential(Conv2d(skip_input, output_features, kernel_size=3, stride=1, padding=1), + nn.GroupNorm(8, output_features), + nn.LeakyReLU(), + Conv2d(output_features, output_features, kernel_size=3, stride=1, padding=1), + nn.GroupNorm(8, output_features), + nn.LeakyReLU()) + + def forward(self, x, concat_with): + up_x = F.interpolate(x, size=[concat_with.size(2), concat_with.size(3)], mode='bilinear', align_corners=True) + f = torch.cat([up_x, concat_with], dim=1) + return self._net(f) + + +# Conv2d with weight standardization +class Conv2d(nn.Conv2d): + def __init__(self, in_channels, out_channels, kernel_size, stride=1, + padding=0, dilation=1, groups=1, bias=True): + super(Conv2d, self).__init__(in_channels, out_channels, kernel_size, stride, + padding, dilation, groups, bias) + + def forward(self, x): + weight = self.weight + weight_mean = weight.mean(dim=1, keepdim=True).mean(dim=2, + keepdim=True).mean(dim=3, keepdim=True) + weight = weight - weight_mean + std = weight.view(weight.size(0), -1).std(dim=1).view(-1, 1, 1, 1) + 1e-5 + weight = weight / std.expand_as(weight) + return F.conv2d(x, weight, self.bias, self.stride, + self.padding, self.dilation, self.groups) + + +# normalize +def norm_normalize(norm_out): + min_kappa = 0.01 + norm_x, norm_y, norm_z, kappa = torch.split(norm_out, 1, dim=1) + norm = torch.sqrt(norm_x ** 2.0 + norm_y ** 2.0 + norm_z ** 2.0) + 1e-10 + kappa = F.elu(kappa) + 1.0 + min_kappa + final_out = torch.cat([norm_x / norm, norm_y / norm, norm_z / norm, kappa], dim=1) + return final_out + + +# uncertainty-guided sampling (only used during training) +@torch.no_grad() +def sample_points(init_normal, gt_norm_mask, sampling_ratio, beta): + device = init_normal.device + B, _, H, W = init_normal.shape + N = int(sampling_ratio * H * W) + beta = beta + + # uncertainty map + uncertainty_map = -1 * init_normal[:, 3, :, :] # B, H, W + + # gt_invalid_mask (B, H, W) + if gt_norm_mask is not None: + gt_invalid_mask = F.interpolate(gt_norm_mask.float(), size=[H, W], mode='nearest') + gt_invalid_mask = gt_invalid_mask[:, 0, :, :] < 0.5 + uncertainty_map[gt_invalid_mask] = -1e4 + + # (B, H*W) + _, idx = uncertainty_map.view(B, -1).sort(1, descending=True) + + # importance sampling + if int(beta * N) > 0: + importance = idx[:, :int(beta * N)] # B, beta*N + + # remaining + remaining = idx[:, int(beta * N):] # B, H*W - beta*N + + # coverage + num_coverage = N - int(beta * N) + + if num_coverage <= 0: + samples = importance + else: + coverage_list = [] + for i in range(B): + idx_c = torch.randperm(remaining.size()[1]) # shuffles "H*W - beta*N" + coverage_list.append(remaining[i, :][idx_c[:num_coverage]].view(1, -1)) # 1, N-beta*N + coverage = torch.cat(coverage_list, dim=0) # B, N-beta*N + samples = torch.cat((importance, coverage), dim=1) # B, N + + else: + # remaining + remaining = idx[:, :] # B, H*W + + # coverage + num_coverage = N + + coverage_list = [] + for i in range(B): + idx_c = torch.randperm(remaining.size()[1]) # shuffles "H*W - beta*N" + coverage_list.append(remaining[i, :][idx_c[:num_coverage]].view(1, -1)) # 1, N-beta*N + coverage = torch.cat(coverage_list, dim=0) # B, N-beta*N + samples = coverage + + # point coordinates + rows_int = samples // W # 0 for first row, H-1 for last row + rows_float = rows_int / float(H-1) # 0 to 1.0 + rows_float = (rows_float * 2.0) - 1.0 # -1.0 to 1.0 + + cols_int = samples % W # 0 for first column, W-1 for last column + cols_float = cols_int / float(W-1) # 0 to 1.0 + cols_float = (cols_float * 2.0) - 1.0 # -1.0 to 1.0 + + point_coords = torch.zeros(B, 1, N, 2) + point_coords[:, 0, :, 0] = cols_float # x coord + point_coords[:, 0, :, 1] = rows_float # y coord + point_coords = point_coords.to(device) + return point_coords, rows_int, cols_int \ No newline at end of file diff --git a/invokeai/backend/image_util/pbr_maps/architecture/block.py b/invokeai/backend/image_util/pbr_maps/architecture/block.py new file mode 100644 index 00000000000..225d606563d --- /dev/null +++ b/invokeai/backend/image_util/pbr_maps/architecture/block.py @@ -0,0 +1,367 @@ +# Original: https://github.com/joeyballentine/Material-Map-Generator +# Adopted and optimized for Invoke AI + +from collections import OrderedDict +from typing import Any, List, Literal, Optional + +import torch +import torch.nn as nn + +ACTIVATION_LAYER_TYPE = Literal["relu", "leakyrelu", "prelu"] +NORMALIZATION_LAYER_TYPE = Literal["batch", "instance"] +PADDING_LAYER_TYPE = Literal["zero", "reflect", "replicate"] +BLOCK_MODE = Literal["CNA", "NAC", "CNAC"] +UPCONV_BLOCK_MODE = Literal["nearest", "linear", "bilinear", "bicubic", "trilinear"] + + +def act(act_type: ACTIVATION_LAYER_TYPE, inplace: bool = True, neg_slope: float = 0.2, n_prelu: int = 1): + """Helper to select Activation Layer""" + if act_type == "relu": + layer = nn.ReLU(inplace) + elif act_type == "leakyrelu": + layer = nn.LeakyReLU(neg_slope, inplace) + elif act_type == "prelu": + layer = nn.PReLU(num_parameters=n_prelu, init=neg_slope) + return layer + + +def norm(norm_type: NORMALIZATION_LAYER_TYPE, nc: int): + """Helper to select Normalization Layer""" + if norm_type == "batch": + layer = nn.BatchNorm2d(nc, affine=True) + elif norm_type == "instance": + layer = nn.InstanceNorm2d(nc, affine=False) + return layer + + +def pad(pad_type: PADDING_LAYER_TYPE, padding: int): + """Helper to select Padding Layer""" + if padding == 0 or pad_type == "zero": + return None + if pad_type == "reflect": + layer = nn.ReflectionPad2d(padding) + elif pad_type == "replicate": + layer = nn.ReplicationPad2d(padding) + return layer + + +def get_valid_padding(kernel_size: int, dilation: int): + kernel_size = kernel_size + (kernel_size - 1) * (dilation - 1) + padding = (kernel_size - 1) // 2 + return padding + + +def sequential(*args: Any): + # Flatten Sequential. It unwraps nn.Sequential. + if len(args) == 1: + if isinstance(args[0], OrderedDict): + raise NotImplementedError("sequential does not support OrderedDict input.") + return args[0] # No sequential is needed. + modules: List[nn.Module] = [] + for module in args: + if isinstance(module, nn.Sequential): + for submodule in module.children(): + modules.append(submodule) + elif isinstance(module, nn.Module): + modules.append(module) + return nn.Sequential(*modules) + + +def conv_block( + in_nc: int, + out_nc: int, + kernel_size: int, + stride: int = 1, + dilation: int = 1, + groups: int = 1, + bias: bool = True, + pad_type: Optional[PADDING_LAYER_TYPE] = "zero", + norm_type: Optional[NORMALIZATION_LAYER_TYPE] = None, + act_type: Optional[ACTIVATION_LAYER_TYPE] = "relu", + mode: BLOCK_MODE = "CNA", +): + """ + Conv layer with padding, normalization, activation + mode: CNA --> Conv -> Norm -> Act + NAC --> Norm -> Act --> Conv (Identity Mappings in Deep Residual Networks, ECCV16) + """ + assert mode in ["CNA", "NAC", "CNAC"], f"Wrong conv mode [{mode}]" + padding = get_valid_padding(kernel_size, dilation) + p = pad(pad_type, padding) if pad_type else None + padding = padding if pad_type == "zero" else 0 + + c = nn.Conv2d( + in_nc, + out_nc, + kernel_size=kernel_size, + stride=stride, + padding=padding, + dilation=dilation, + bias=bias, + groups=groups, + ) + a = act(act_type) if act_type else None + match mode: + case "CNA": + n = norm(norm_type, out_nc) if norm_type else None + return sequential(p, c, n, a) + case "NAC": + if norm_type is None and act_type is not None: + a = act(act_type, inplace=False) + n = norm(norm_type, in_nc) if norm_type else None + return sequential(n, a, p, c) + case "CNAC": + n = norm(norm_type, in_nc) if norm_type else None + return sequential(n, a, p, c) + + +class ConcatBlock(nn.Module): + # Concat the output of a submodule to its input + def __init__(self, submodule: nn.Module): + super(ConcatBlock, self).__init__() + self.sub = submodule + + def forward(self, x: torch.Tensor): + output = torch.cat((x, self.sub(x)), dim=1) + return output + + def __repr__(self): + tmpstr = "Identity .. \n|" + modstr = self.sub.__repr__().replace("\n", "\n|") + tmpstr = tmpstr + modstr + return tmpstr + + +class ShortcutBlock(nn.Module): + # Elementwise sum the output of a submodule to its input + def __init__(self, submodule: nn.Module): + super(ShortcutBlock, self).__init__() + self.sub = submodule + + def forward(self, x: torch.Tensor): + output = x + self.sub(x) + return output + + def __repr__(self): + tmpstr = "Identity + \n|" + modstr = self.sub.__repr__().replace("\n", "\n|") + tmpstr = tmpstr + modstr + return tmpstr + + +class ShortcutBlockSPSR(nn.Module): + # Elementwise sum the output of a submodule to its input + def __init__(self, submodule: nn.Module): + super(ShortcutBlockSPSR, self).__init__() + self.sub = submodule + + def forward(self, x: torch.Tensor): + return x, self.sub + + def __repr__(self): + tmpstr = "Identity + \n|" + modstr = self.sub.__repr__().replace("\n", "\n|") + tmpstr = tmpstr + modstr + return tmpstr + + +class ResNetBlock(nn.Module): + """ + ResNet Block, 3-3 style + with extra residual scaling used in EDSR + (Enhanced Deep Residual Networks for Single Image Super-Resolution, CVPRW 17) + """ + + def __init__( + self, + in_nc: int, + mid_nc: int, + out_nc: int, + kernel_size: int = 3, + stride: int = 1, + dilation: int = 1, + groups: int = 1, + bias: bool = True, + pad_type: PADDING_LAYER_TYPE = "zero", + norm_type: Optional[NORMALIZATION_LAYER_TYPE] = None, + act_type: Optional[ACTIVATION_LAYER_TYPE] = "relu", + mode: BLOCK_MODE = "CNA", + res_scale: int = 1, + ): + super(ResNetBlock, self).__init__() + conv0 = conv_block( + in_nc, mid_nc, kernel_size, stride, dilation, groups, bias, pad_type, norm_type, act_type, mode + ) + if mode == "CNA": + act_type = None + if mode == "CNAC": # Residual path: |-CNAC-| + act_type = None + norm_type = None + conv1 = conv_block( + mid_nc, out_nc, kernel_size, stride, dilation, groups, bias, pad_type, norm_type, act_type, mode + ) + + self.res = sequential(conv0, conv1) + self.res_scale = res_scale + + def forward(self, x: torch.Tensor): + res = self.res(x).mul(self.res_scale) + return x + res + + +class ResidualDenseBlock_5C(nn.Module): + """ + Residual Dense Block + style: 5 convs + The core module of paper: (Residual Dense Network for Image Super-Resolution, CVPR 18) + """ + + def __init__( + self, + nc: int, + kernel_size: int = 3, + gc: int = 32, + stride: int = 1, + bias: bool = True, + pad_type: PADDING_LAYER_TYPE = "zero", + norm_type: Optional[NORMALIZATION_LAYER_TYPE] = None, + act_type: ACTIVATION_LAYER_TYPE = "leakyrelu", + mode: BLOCK_MODE = "CNA", + ): + super(ResidualDenseBlock_5C, self).__init__() + # gc: growth channel, i.e. intermediate channels + self.conv1 = conv_block( + nc, gc, kernel_size, stride, bias=bias, pad_type=pad_type, norm_type=norm_type, act_type=act_type, mode=mode + ) + self.conv2 = conv_block( + nc + gc, + gc, + kernel_size, + stride, + bias=bias, + pad_type=pad_type, + norm_type=norm_type, + act_type=act_type, + mode=mode, + ) + self.conv3 = conv_block( + nc + 2 * gc, + gc, + kernel_size, + stride, + bias=bias, + pad_type=pad_type, + norm_type=norm_type, + act_type=act_type, + mode=mode, + ) + self.conv4 = conv_block( + nc + 3 * gc, + gc, + kernel_size, + stride, + bias=bias, + pad_type=pad_type, + norm_type=norm_type, + act_type=act_type, + mode=mode, + ) + if mode == "CNA": + last_act = None + else: + last_act = act_type + self.conv5 = conv_block( + nc + 4 * gc, nc, 3, stride, bias=bias, pad_type=pad_type, norm_type=norm_type, act_type=last_act, mode=mode + ) + + def forward(self, x: torch.Tensor): + x1 = self.conv1(x) + x2 = self.conv2(torch.cat((x, x1), 1)) + x3 = self.conv3(torch.cat((x, x1, x2), 1)) + x4 = self.conv4(torch.cat((x, x1, x2, x3), 1)) + x5 = self.conv5(torch.cat((x, x1, x2, x3, x4), 1)) + return x5.mul(0.2) + x + + +class RRDB(nn.Module): + """ + Residual in Residual Dense Block + (ESRGAN: Enhanced Super-Resolution Generative Adversarial Networks) + """ + + def __init__( + self, + nc: int, + kernel_size: int = 3, + gc: int = 32, + stride: int = 1, + bias: bool = True, + pad_type: PADDING_LAYER_TYPE = "zero", + norm_type: Optional[NORMALIZATION_LAYER_TYPE] = None, + act_type: ACTIVATION_LAYER_TYPE = "leakyrelu", + mode: BLOCK_MODE = "CNA", + ): + super(RRDB, self).__init__() + self.RDB1 = ResidualDenseBlock_5C(nc, kernel_size, gc, stride, bias, pad_type, norm_type, act_type, mode) + self.RDB2 = ResidualDenseBlock_5C(nc, kernel_size, gc, stride, bias, pad_type, norm_type, act_type, mode) + self.RDB3 = ResidualDenseBlock_5C(nc, kernel_size, gc, stride, bias, pad_type, norm_type, act_type, mode) + + def forward(self, x: torch.Tensor): + out = self.RDB1(x) + out = self.RDB2(out) + out = self.RDB3(out) + return out.mul(0.2) + x + + +# Upsampler +def pixelshuffle_block( + in_nc: int, + out_nc: int, + upscale_factor: int = 2, + kernel_size: int = 3, + stride: int = 1, + bias: bool = True, + pad_type: PADDING_LAYER_TYPE = "zero", + norm_type: Optional[NORMALIZATION_LAYER_TYPE] = None, + act_type: ACTIVATION_LAYER_TYPE = "relu", +): + """ + Pixel shuffle layer + (Real-Time Single Image and Video Super-Resolution Using an Efficient Sub-Pixel Convolutional + Neural Network, CVPR17) + """ + conv = conv_block( + in_nc, + out_nc * (upscale_factor**2), + kernel_size, + stride, + bias=bias, + pad_type=pad_type, + norm_type=None, + act_type=None, + ) + pixel_shuffle = nn.PixelShuffle(upscale_factor) + + n = norm(norm_type, out_nc) if norm_type else None + a = act(act_type) if act_type else None + return sequential(conv, pixel_shuffle, n, a) + + +def upconv_block( + in_nc: int, + out_nc: int, + upscale_factor: int = 2, + kernel_size: int = 3, + stride: int = 1, + bias: bool = True, + pad_type: PADDING_LAYER_TYPE = "zero", + norm_type: Optional[NORMALIZATION_LAYER_TYPE] = None, + act_type: ACTIVATION_LAYER_TYPE = "relu", + mode: UPCONV_BLOCK_MODE = "nearest", +): + # Adopted from https://distill.pub/2016/deconv-checkerboard/ + upsample = nn.Upsample(scale_factor=upscale_factor, mode=mode) + conv = conv_block( + in_nc, out_nc, kernel_size, stride, bias=bias, pad_type=pad_type, norm_type=norm_type, act_type=act_type + ) + return sequential(upsample, conv) diff --git a/invokeai/backend/image_util/pbr_maps/architecture/pbr_rrdb_net.py b/invokeai/backend/image_util/pbr_maps/architecture/pbr_rrdb_net.py new file mode 100644 index 00000000000..14f597c6d45 --- /dev/null +++ b/invokeai/backend/image_util/pbr_maps/architecture/pbr_rrdb_net.py @@ -0,0 +1,70 @@ +# Original: https://github.com/joeyballentine/Material-Map-Generator +# Adopted and optimized for Invoke AI + +import math +from typing import Literal, Optional + +import torch +import torch.nn as nn + +import invokeai.backend.image_util.pbr_maps.architecture.block as B + +UPSCALE_MODE = Literal["upconv", "pixelshuffle"] + + +class PBR_RRDB_Net(nn.Module): + def __init__( + self, + in_nc: int, + out_nc: int, + nf: int, + nb: int, + gc: int = 32, + upscale: int = 4, + norm_type: Optional[B.NORMALIZATION_LAYER_TYPE] = None, + act_type: B.ACTIVATION_LAYER_TYPE = "leakyrelu", + mode: B.BLOCK_MODE = "CNA", + res_scale: int = 1, + upsample_mode: UPSCALE_MODE = "upconv", + ): + super(PBR_RRDB_Net, self).__init__() + n_upscale = int(math.log(upscale, 2)) + if upscale == 3: + n_upscale = 1 + + fea_conv = B.conv_block(in_nc, nf, kernel_size=3, norm_type=None, act_type=None) + rb_blocks = [ + B.RRDB( + nf, + kernel_size=3, + gc=32, + stride=1, + bias=True, + pad_type="zero", + norm_type=norm_type, + act_type=act_type, + mode="CNA", + ) + for _ in range(nb) + ] + LR_conv = B.conv_block(nf, nf, kernel_size=3, norm_type=norm_type, act_type=None, mode=mode) + + if upsample_mode == "upconv": + upsample_block = B.upconv_block + elif upsample_mode == "pixelshuffle": + upsample_block = B.pixelshuffle_block + + if upscale == 3: + upsampler = upsample_block(nf, nf, 3, act_type=act_type) + else: + upsampler = [upsample_block(nf, nf, act_type=act_type) for _ in range(n_upscale)] + + HR_conv0 = B.conv_block(nf, nf, kernel_size=3, norm_type=None, act_type=act_type) + HR_conv1 = B.conv_block(nf, out_nc, kernel_size=3, norm_type=None, act_type=None) + + self.model = B.sequential( + fea_conv, B.ShortcutBlock(B.sequential(*rb_blocks, LR_conv)), *upsampler, HR_conv0, HR_conv1 + ) + + def forward(self, x: torch.Tensor): + return self.model(x) diff --git a/invokeai/backend/image_util/pbr_maps/pbr_maps.py b/invokeai/backend/image_util/pbr_maps/pbr_maps.py new file mode 100644 index 00000000000..1db57091028 --- /dev/null +++ b/invokeai/backend/image_util/pbr_maps/pbr_maps.py @@ -0,0 +1,141 @@ +# Original: https://github.com/joeyballentine/Material-Map-Generator +# Adopted and optimized for Invoke AI + +import pathlib +from typing import Any, Literal + +import cv2 +import numpy as np +import numpy.typing as npt +import torch +from PIL import Image +from safetensors.torch import load_file + +from invokeai.backend.image_util.pbr_maps.architecture.pbr_rrdb_net import PBR_RRDB_Net +from invokeai.backend.image_util.pbr_maps.utils.image_ops import crop_seamless, esrgan_launcher_split_merge + +NORMAL_MAP_MODEL = ( + "https://huggingface.co/InvokeAI/pbr-material-maps/resolve/main/normal_map_generator.safetensors?download=true" +) +OTHER_MAP_MODEL = ( + "https://huggingface.co/InvokeAI/pbr-material-maps/resolve/main/franken_map_generator.safetensors?download=true" +) + + +class PBRMapsGenerator: + def __init__(self, normal_map_model: PBR_RRDB_Net, other_map_model: PBR_RRDB_Net, device: torch.device) -> None: + self.normal_map_model = normal_map_model + self.other_map_model = other_map_model + self.device = device + + @staticmethod + def load_model(model_path: pathlib.Path, device: torch.device) -> PBR_RRDB_Net: + state_dict = load_file(model_path.as_posix(), device=device.type) + + model = PBR_RRDB_Net( + 3, + 3, + 32, + 12, + gc=32, + upscale=1, + norm_type=None, + act_type="leakyrelu", + mode="CNA", + res_scale=1, + upsample_mode="upconv", + ) + + model.load_state_dict(state_dict, strict=False) + + del state_dict + if torch.cuda.is_available() and device.type == "cuda": + torch.cuda.empty_cache() + + model.eval() + + for _, v in model.named_parameters(): + v.requires_grad = False + + return model.to(device) + + def process(self, img: npt.NDArray[Any], model: PBR_RRDB_Net): + img = img.astype(np.float32) / np.iinfo(img.dtype).max + img = img[..., ::-1].copy() + tensor_img = torch.tensor(img).permute(2, 0, 1).unsqueeze(0).to(self.device) + + with torch.no_grad(): + output = model(tensor_img).data.squeeze(0).float().cpu().clamp_(0, 1).numpy() + output = output[[2, 1, 0], :, :] + output = np.transpose(output, (1, 2, 0)) + output = (output * 255.0).round() + return output + + def _cv2_to_pil(self, image: npt.NDArray[Any]): + return Image.fromarray(cv2.cvtColor(image.astype(np.uint8), cv2.COLOR_RGB2BGR)) + + def generate_maps( + self, + image: Image.Image, + tile_size: int = 512, + border_mode: Literal["none", "seamless", "mirror", "replicate"] = "none", + ): + """ + Generate PBR texture maps (normal, roughness, and displacement) from an input image. + The image can optionally be padded before inference to control how borders are treated, + which can help create seamless or edge‑consistent textures. + + Args: + image: Source image used to generate the PBR maps. + tile_size: Maximum tile size used for tiled inference. If the image is larger than + this size in either dimension, it will be split into tiles for processing and + then merged. + + border_mode: Strategy for padding the image before inference: + - "none": No padding is applied; the image is processed as‑is. + - "seamless": Pads the image using wrap‑around tiling + (`cv2.BORDER_WRAP`) to help produce seamless textures. + - "mirror": Pads the image by mirroring border pixels + (`cv2.BORDER_REFLECT_101`) to reduce edge artifacts. + - "replicate": Pads the image by replicating the edge pixels outward + (`cv2.BORDER_REPLICATE`). + + Returns: + A tuple of three PIL Images: + - normal_map: RGB normal map generated from the input. + - roughness: Single‑channel roughness map extracted from the second model output. + - displacement: Single‑channel displacement (height) map extracted from the + second model output. + """ + + models = [self.normal_map_model, self.other_map_model] + np_image = np.array(image).astype(np.uint8) + + match border_mode: + case "seamless": + np_image = cv2.copyMakeBorder(np_image, 16, 16, 16, 16, cv2.BORDER_WRAP) + case "mirror": + np_image = cv2.copyMakeBorder(np_image, 16, 16, 16, 16, cv2.BORDER_REFLECT_101) + case "replicate": + np_image = cv2.copyMakeBorder(np_image, 16, 16, 16, 16, cv2.BORDER_REPLICATE) + case "none": + pass + + img_height, img_width = np_image.shape[:2] + + # Checking whether to perform tiled inference + do_split = img_height > tile_size or img_width > tile_size + + if do_split: + rlts = esrgan_launcher_split_merge(np_image, self.process, models, scale_factor=1, tile_size=tile_size) + else: + rlts = [self.process(np_image, model) for model in models] + + if border_mode != "none": + rlts = [crop_seamless(rlt) for rlt in rlts] + + normal_map = self._cv2_to_pil(rlts[0]) + roughness = self._cv2_to_pil(rlts[1][:, :, 1]) + displacement = self._cv2_to_pil(rlts[1][:, :, 0]) + + return normal_map, roughness, displacement diff --git a/invokeai/backend/image_util/pbr_maps/utils/image_ops.py b/invokeai/backend/image_util/pbr_maps/utils/image_ops.py new file mode 100644 index 00000000000..426620797cb --- /dev/null +++ b/invokeai/backend/image_util/pbr_maps/utils/image_ops.py @@ -0,0 +1,93 @@ +# Original: https://github.com/joeyballentine/Material-Map-Generator +# Adopted and optimized for Invoke AI + +import math +from typing import Any, Callable, List + +import numpy as np +import numpy.typing as npt + +from invokeai.backend.image_util.pbr_maps.architecture.pbr_rrdb_net import PBR_RRDB_Net + + +def crop_seamless(img: npt.NDArray[Any]): + img_height, img_width = img.shape[:2] + y, x = 16, 16 + h, w = img_height - 32, img_width - 32 + img = img[y : y + h, x : x + w] + return img + + +# from https://github.com/ata4/esrgan-launcher/blob/master/upscale.py +def esrgan_launcher_split_merge( + input_image: npt.NDArray[Any], + upscale_function: Callable[[npt.NDArray[Any], PBR_RRDB_Net], npt.NDArray[Any]], + models: List[PBR_RRDB_Net], + scale_factor: int = 4, + tile_size: int = 512, + tile_padding: float = 0.125, +): + width, height, depth = input_image.shape + output_width = width * scale_factor + output_height = height * scale_factor + output_shape = (output_width, output_height, depth) + + # start with black image + output_images = [np.zeros(output_shape, np.uint8) for _ in range(len(models))] + + tile_padding = math.ceil(tile_size * tile_padding) + tile_size = math.ceil(tile_size / scale_factor) + + tiles_x = math.ceil(width / tile_size) + tiles_y = math.ceil(height / tile_size) + + for y in range(tiles_y): + for x in range(tiles_x): + # extract tile from input image + ofs_x = x * tile_size + ofs_y = y * tile_size + + # input tile area on total image + input_start_x = ofs_x + input_end_x = min(ofs_x + tile_size, width) + + input_start_y = ofs_y + input_end_y = min(ofs_y + tile_size, height) + + # input tile area on total image with padding + input_start_x_pad = max(input_start_x - tile_padding, 0) + input_end_x_pad = min(input_end_x + tile_padding, width) + + input_start_y_pad = max(input_start_y - tile_padding, 0) + input_end_y_pad = min(input_end_y + tile_padding, height) + + # input tile dimensions + input_tile_width = input_end_x - input_start_x + input_tile_height = input_end_y - input_start_y + + input_tile = input_image[input_start_x_pad:input_end_x_pad, input_start_y_pad:input_end_y_pad] + + for idx, model in enumerate(models): + # upscale tile + output_tile = upscale_function(input_tile, model) + + # output tile area on total image + output_start_x = input_start_x * scale_factor + output_end_x = input_end_x * scale_factor + + output_start_y = input_start_y * scale_factor + output_end_y = input_end_y * scale_factor + + # output tile area without padding + output_start_x_tile = (input_start_x - input_start_x_pad) * scale_factor + output_end_x_tile = output_start_x_tile + input_tile_width * scale_factor + + output_start_y_tile = (input_start_y - input_start_y_pad) * scale_factor + output_end_y_tile = output_start_y_tile + input_tile_height * scale_factor + + # put tile into output image + output_images[idx][output_start_x:output_end_x, output_start_y:output_end_y] = output_tile[ + output_start_x_tile:output_end_x_tile, output_start_y_tile:output_end_y_tile + ] + + return output_images diff --git a/invokeai/backend/image_util/pidi/__init__.py b/invokeai/backend/image_util/pidi/__init__.py new file mode 100644 index 00000000000..63df7b6058e --- /dev/null +++ b/invokeai/backend/image_util/pidi/__init__.py @@ -0,0 +1,80 @@ +# Adapted from https://github.com/huggingface/controlnet_aux + +import pathlib + +import cv2 +import huggingface_hub +import numpy as np +import torch +from einops import rearrange +from PIL import Image + +from invokeai.backend.image_util.pidi.model import PiDiNet, pidinet +from invokeai.backend.image_util.util import nms, normalize_image_channel_count, np_to_pil, pil_to_np, safe_step +from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device + + +class PIDINetDetector: + """Simple wrapper around a PiDiNet model for edge detection.""" + + hf_repo_id = "lllyasviel/Annotators" + hf_filename = "table5_pidinet.pth" + + @classmethod + def get_model_url(cls) -> str: + """Get the URL to download the model from the Hugging Face Hub.""" + return huggingface_hub.hf_hub_url(cls.hf_repo_id, cls.hf_filename) + + @classmethod + def load_model(cls, model_path: pathlib.Path) -> PiDiNet: + """Load the model from a file.""" + + model = pidinet() + model.load_state_dict({k.replace("module.", ""): v for k, v in torch.load(model_path)["state_dict"].items()}) + model.eval() + return model + + def __init__(self, model: PiDiNet) -> None: + self.model = model + + def to(self, device: torch.device): + self.model.to(device) + return self + + def run( + self, image: Image.Image, quantize_edges: bool = False, scribble: bool = False, apply_filter: bool = False + ) -> Image.Image: + """Processes an image and returns the detected edges.""" + + device = get_effective_device(self.model) + + np_img = pil_to_np(image) + np_img = normalize_image_channel_count(np_img) + + assert np_img.ndim == 3 + + bgr_img = np_img[:, :, ::-1].copy() + + with torch.no_grad(): + image_pidi = torch.from_numpy(bgr_img).float().to(device) + image_pidi = image_pidi / 255.0 + image_pidi = rearrange(image_pidi, "h w c -> 1 c h w") + edge = self.model(image_pidi)[-1] + edge = edge.cpu().numpy() + if apply_filter: + edge = edge > 0.5 + if quantize_edges: + edge = safe_step(edge) + edge = (edge * 255.0).clip(0, 255).astype(np.uint8) + + detected_map = edge[0, 0] + + if scribble: + detected_map = nms(detected_map, 127, 3.0) + detected_map = cv2.GaussianBlur(detected_map, (0, 0), 3.0) + detected_map[detected_map > 4] = 255 + detected_map[detected_map < 255] = 0 + + output_img = np_to_pil(detected_map) + + return output_img diff --git a/invokeai/backend/image_util/pidi/model.py b/invokeai/backend/image_util/pidi/model.py new file mode 100644 index 00000000000..16595b35a4f --- /dev/null +++ b/invokeai/backend/image_util/pidi/model.py @@ -0,0 +1,681 @@ +""" +Author: Zhuo Su, Wenzhe Liu +Date: Feb 18, 2021 +""" + +import math + +import cv2 +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F + + +def img2tensor(imgs, bgr2rgb=True, float32=True): + """Numpy array to tensor. + + Args: + imgs (list[ndarray] | ndarray): Input images. + bgr2rgb (bool): Whether to change bgr to rgb. + float32 (bool): Whether to change to float32. + + Returns: + list[tensor] | tensor: Tensor images. If returned results only have + one element, just return tensor. + """ + + def _totensor(img, bgr2rgb, float32): + if img.shape[2] == 3 and bgr2rgb: + if img.dtype == 'float64': + img = img.astype('float32') + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + img = torch.from_numpy(img.transpose(2, 0, 1)) + if float32: + img = img.float() + return img + + if isinstance(imgs, list): + return [_totensor(img, bgr2rgb, float32) for img in imgs] + else: + return _totensor(imgs, bgr2rgb, float32) + +nets = { + 'baseline': { + 'layer0': 'cv', + 'layer1': 'cv', + 'layer2': 'cv', + 'layer3': 'cv', + 'layer4': 'cv', + 'layer5': 'cv', + 'layer6': 'cv', + 'layer7': 'cv', + 'layer8': 'cv', + 'layer9': 'cv', + 'layer10': 'cv', + 'layer11': 'cv', + 'layer12': 'cv', + 'layer13': 'cv', + 'layer14': 'cv', + 'layer15': 'cv', + }, + 'c-v15': { + 'layer0': 'cd', + 'layer1': 'cv', + 'layer2': 'cv', + 'layer3': 'cv', + 'layer4': 'cv', + 'layer5': 'cv', + 'layer6': 'cv', + 'layer7': 'cv', + 'layer8': 'cv', + 'layer9': 'cv', + 'layer10': 'cv', + 'layer11': 'cv', + 'layer12': 'cv', + 'layer13': 'cv', + 'layer14': 'cv', + 'layer15': 'cv', + }, + 'a-v15': { + 'layer0': 'ad', + 'layer1': 'cv', + 'layer2': 'cv', + 'layer3': 'cv', + 'layer4': 'cv', + 'layer5': 'cv', + 'layer6': 'cv', + 'layer7': 'cv', + 'layer8': 'cv', + 'layer9': 'cv', + 'layer10': 'cv', + 'layer11': 'cv', + 'layer12': 'cv', + 'layer13': 'cv', + 'layer14': 'cv', + 'layer15': 'cv', + }, + 'r-v15': { + 'layer0': 'rd', + 'layer1': 'cv', + 'layer2': 'cv', + 'layer3': 'cv', + 'layer4': 'cv', + 'layer5': 'cv', + 'layer6': 'cv', + 'layer7': 'cv', + 'layer8': 'cv', + 'layer9': 'cv', + 'layer10': 'cv', + 'layer11': 'cv', + 'layer12': 'cv', + 'layer13': 'cv', + 'layer14': 'cv', + 'layer15': 'cv', + }, + 'cvvv4': { + 'layer0': 'cd', + 'layer1': 'cv', + 'layer2': 'cv', + 'layer3': 'cv', + 'layer4': 'cd', + 'layer5': 'cv', + 'layer6': 'cv', + 'layer7': 'cv', + 'layer8': 'cd', + 'layer9': 'cv', + 'layer10': 'cv', + 'layer11': 'cv', + 'layer12': 'cd', + 'layer13': 'cv', + 'layer14': 'cv', + 'layer15': 'cv', + }, + 'avvv4': { + 'layer0': 'ad', + 'layer1': 'cv', + 'layer2': 'cv', + 'layer3': 'cv', + 'layer4': 'ad', + 'layer5': 'cv', + 'layer6': 'cv', + 'layer7': 'cv', + 'layer8': 'ad', + 'layer9': 'cv', + 'layer10': 'cv', + 'layer11': 'cv', + 'layer12': 'ad', + 'layer13': 'cv', + 'layer14': 'cv', + 'layer15': 'cv', + }, + 'rvvv4': { + 'layer0': 'rd', + 'layer1': 'cv', + 'layer2': 'cv', + 'layer3': 'cv', + 'layer4': 'rd', + 'layer5': 'cv', + 'layer6': 'cv', + 'layer7': 'cv', + 'layer8': 'rd', + 'layer9': 'cv', + 'layer10': 'cv', + 'layer11': 'cv', + 'layer12': 'rd', + 'layer13': 'cv', + 'layer14': 'cv', + 'layer15': 'cv', + }, + 'cccv4': { + 'layer0': 'cd', + 'layer1': 'cd', + 'layer2': 'cd', + 'layer3': 'cv', + 'layer4': 'cd', + 'layer5': 'cd', + 'layer6': 'cd', + 'layer7': 'cv', + 'layer8': 'cd', + 'layer9': 'cd', + 'layer10': 'cd', + 'layer11': 'cv', + 'layer12': 'cd', + 'layer13': 'cd', + 'layer14': 'cd', + 'layer15': 'cv', + }, + 'aaav4': { + 'layer0': 'ad', + 'layer1': 'ad', + 'layer2': 'ad', + 'layer3': 'cv', + 'layer4': 'ad', + 'layer5': 'ad', + 'layer6': 'ad', + 'layer7': 'cv', + 'layer8': 'ad', + 'layer9': 'ad', + 'layer10': 'ad', + 'layer11': 'cv', + 'layer12': 'ad', + 'layer13': 'ad', + 'layer14': 'ad', + 'layer15': 'cv', + }, + 'rrrv4': { + 'layer0': 'rd', + 'layer1': 'rd', + 'layer2': 'rd', + 'layer3': 'cv', + 'layer4': 'rd', + 'layer5': 'rd', + 'layer6': 'rd', + 'layer7': 'cv', + 'layer8': 'rd', + 'layer9': 'rd', + 'layer10': 'rd', + 'layer11': 'cv', + 'layer12': 'rd', + 'layer13': 'rd', + 'layer14': 'rd', + 'layer15': 'cv', + }, + 'c16': { + 'layer0': 'cd', + 'layer1': 'cd', + 'layer2': 'cd', + 'layer3': 'cd', + 'layer4': 'cd', + 'layer5': 'cd', + 'layer6': 'cd', + 'layer7': 'cd', + 'layer8': 'cd', + 'layer9': 'cd', + 'layer10': 'cd', + 'layer11': 'cd', + 'layer12': 'cd', + 'layer13': 'cd', + 'layer14': 'cd', + 'layer15': 'cd', + }, + 'a16': { + 'layer0': 'ad', + 'layer1': 'ad', + 'layer2': 'ad', + 'layer3': 'ad', + 'layer4': 'ad', + 'layer5': 'ad', + 'layer6': 'ad', + 'layer7': 'ad', + 'layer8': 'ad', + 'layer9': 'ad', + 'layer10': 'ad', + 'layer11': 'ad', + 'layer12': 'ad', + 'layer13': 'ad', + 'layer14': 'ad', + 'layer15': 'ad', + }, + 'r16': { + 'layer0': 'rd', + 'layer1': 'rd', + 'layer2': 'rd', + 'layer3': 'rd', + 'layer4': 'rd', + 'layer5': 'rd', + 'layer6': 'rd', + 'layer7': 'rd', + 'layer8': 'rd', + 'layer9': 'rd', + 'layer10': 'rd', + 'layer11': 'rd', + 'layer12': 'rd', + 'layer13': 'rd', + 'layer14': 'rd', + 'layer15': 'rd', + }, + 'carv4': { + 'layer0': 'cd', + 'layer1': 'ad', + 'layer2': 'rd', + 'layer3': 'cv', + 'layer4': 'cd', + 'layer5': 'ad', + 'layer6': 'rd', + 'layer7': 'cv', + 'layer8': 'cd', + 'layer9': 'ad', + 'layer10': 'rd', + 'layer11': 'cv', + 'layer12': 'cd', + 'layer13': 'ad', + 'layer14': 'rd', + 'layer15': 'cv', + }, + } + +def createConvFunc(op_type): + assert op_type in ['cv', 'cd', 'ad', 'rd'], 'unknown op type: %s' % str(op_type) + if op_type == 'cv': + return F.conv2d + + if op_type == 'cd': + def func(x, weights, bias=None, stride=1, padding=0, dilation=1, groups=1): + assert dilation in [1, 2], 'dilation for cd_conv should be in 1 or 2' + assert weights.size(2) == 3 and weights.size(3) == 3, 'kernel size for cd_conv should be 3x3' + assert padding == dilation, 'padding for cd_conv set wrong' + + weights_c = weights.sum(dim=[2, 3], keepdim=True) + yc = F.conv2d(x, weights_c, stride=stride, padding=0, groups=groups) + y = F.conv2d(x, weights, bias, stride=stride, padding=padding, dilation=dilation, groups=groups) + return y - yc + return func + elif op_type == 'ad': + def func(x, weights, bias=None, stride=1, padding=0, dilation=1, groups=1): + assert dilation in [1, 2], 'dilation for ad_conv should be in 1 or 2' + assert weights.size(2) == 3 and weights.size(3) == 3, 'kernel size for ad_conv should be 3x3' + assert padding == dilation, 'padding for ad_conv set wrong' + + shape = weights.shape + weights = weights.view(shape[0], shape[1], -1) + weights_conv = (weights - weights[:, :, [3, 0, 1, 6, 4, 2, 7, 8, 5]]).view(shape) # clock-wise + y = F.conv2d(x, weights_conv, bias, stride=stride, padding=padding, dilation=dilation, groups=groups) + return y + return func + elif op_type == 'rd': + def func(x, weights, bias=None, stride=1, padding=0, dilation=1, groups=1): + assert dilation in [1, 2], 'dilation for rd_conv should be in 1 or 2' + assert weights.size(2) == 3 and weights.size(3) == 3, 'kernel size for rd_conv should be 3x3' + padding = 2 * dilation + + shape = weights.shape + if weights.is_cuda: + buffer = torch.cuda.FloatTensor(shape[0], shape[1], 5 * 5).fill_(0) + else: + buffer = torch.zeros(shape[0], shape[1], 5 * 5).to(weights.device) + weights = weights.view(shape[0], shape[1], -1) + buffer[:, :, [0, 2, 4, 10, 14, 20, 22, 24]] = weights[:, :, 1:] + buffer[:, :, [6, 7, 8, 11, 13, 16, 17, 18]] = -weights[:, :, 1:] + buffer[:, :, 12] = 0 + buffer = buffer.view(shape[0], shape[1], 5, 5) + y = F.conv2d(x, buffer, bias, stride=stride, padding=padding, dilation=dilation, groups=groups) + return y + return func + else: + print('impossible to be here unless you force that') + return None + +class Conv2d(nn.Module): + def __init__(self, pdc, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=False): + super(Conv2d, self).__init__() + if in_channels % groups != 0: + raise ValueError('in_channels must be divisible by groups') + if out_channels % groups != 0: + raise ValueError('out_channels must be divisible by groups') + self.in_channels = in_channels + self.out_channels = out_channels + self.kernel_size = kernel_size + self.stride = stride + self.padding = padding + self.dilation = dilation + self.groups = groups + self.weight = nn.Parameter(torch.Tensor(out_channels, in_channels // groups, kernel_size, kernel_size)) + if bias: + self.bias = nn.Parameter(torch.Tensor(out_channels)) + else: + self.register_parameter('bias', None) + self.reset_parameters() + self.pdc = pdc + + def reset_parameters(self): + nn.init.kaiming_uniform_(self.weight, a=math.sqrt(5)) + if self.bias is not None: + fan_in, _ = nn.init._calculate_fan_in_and_fan_out(self.weight) + bound = 1 / math.sqrt(fan_in) + nn.init.uniform_(self.bias, -bound, bound) + + def forward(self, input): + + return self.pdc(input, self.weight, self.bias, self.stride, self.padding, self.dilation, self.groups) + +class CSAM(nn.Module): + """ + Compact Spatial Attention Module + """ + def __init__(self, channels): + super(CSAM, self).__init__() + + mid_channels = 4 + self.relu1 = nn.ReLU() + self.conv1 = nn.Conv2d(channels, mid_channels, kernel_size=1, padding=0) + self.conv2 = nn.Conv2d(mid_channels, 1, kernel_size=3, padding=1, bias=False) + self.sigmoid = nn.Sigmoid() + nn.init.constant_(self.conv1.bias, 0) + + def forward(self, x): + y = self.relu1(x) + y = self.conv1(y) + y = self.conv2(y) + y = self.sigmoid(y) + + return x * y + +class CDCM(nn.Module): + """ + Compact Dilation Convolution based Module + """ + def __init__(self, in_channels, out_channels): + super(CDCM, self).__init__() + + self.relu1 = nn.ReLU() + self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, padding=0) + self.conv2_1 = nn.Conv2d(out_channels, out_channels, kernel_size=3, dilation=5, padding=5, bias=False) + self.conv2_2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, dilation=7, padding=7, bias=False) + self.conv2_3 = nn.Conv2d(out_channels, out_channels, kernel_size=3, dilation=9, padding=9, bias=False) + self.conv2_4 = nn.Conv2d(out_channels, out_channels, kernel_size=3, dilation=11, padding=11, bias=False) + nn.init.constant_(self.conv1.bias, 0) + + def forward(self, x): + x = self.relu1(x) + x = self.conv1(x) + x1 = self.conv2_1(x) + x2 = self.conv2_2(x) + x3 = self.conv2_3(x) + x4 = self.conv2_4(x) + return x1 + x2 + x3 + x4 + + +class MapReduce(nn.Module): + """ + Reduce feature maps into a single edge map + """ + def __init__(self, channels): + super(MapReduce, self).__init__() + self.conv = nn.Conv2d(channels, 1, kernel_size=1, padding=0) + nn.init.constant_(self.conv.bias, 0) + + def forward(self, x): + return self.conv(x) + + +class PDCBlock(nn.Module): + def __init__(self, pdc, inplane, ouplane, stride=1): + super(PDCBlock, self).__init__() + self.stride=stride + + self.stride=stride + if self.stride > 1: + self.pool = nn.MaxPool2d(kernel_size=2, stride=2) + self.shortcut = nn.Conv2d(inplane, ouplane, kernel_size=1, padding=0) + self.conv1 = Conv2d(pdc, inplane, inplane, kernel_size=3, padding=1, groups=inplane, bias=False) + self.relu2 = nn.ReLU() + self.conv2 = nn.Conv2d(inplane, ouplane, kernel_size=1, padding=0, bias=False) + + def forward(self, x): + if self.stride > 1: + x = self.pool(x) + y = self.conv1(x) + y = self.relu2(y) + y = self.conv2(y) + if self.stride > 1: + x = self.shortcut(x) + y = y + x + return y + +class PDCBlock_converted(nn.Module): + """ + CPDC, APDC can be converted to vanilla 3x3 convolution + RPDC can be converted to vanilla 5x5 convolution + """ + def __init__(self, pdc, inplane, ouplane, stride=1): + super(PDCBlock_converted, self).__init__() + self.stride=stride + + if self.stride > 1: + self.pool = nn.MaxPool2d(kernel_size=2, stride=2) + self.shortcut = nn.Conv2d(inplane, ouplane, kernel_size=1, padding=0) + if pdc == 'rd': + self.conv1 = nn.Conv2d(inplane, inplane, kernel_size=5, padding=2, groups=inplane, bias=False) + else: + self.conv1 = nn.Conv2d(inplane, inplane, kernel_size=3, padding=1, groups=inplane, bias=False) + self.relu2 = nn.ReLU() + self.conv2 = nn.Conv2d(inplane, ouplane, kernel_size=1, padding=0, bias=False) + + def forward(self, x): + if self.stride > 1: + x = self.pool(x) + y = self.conv1(x) + y = self.relu2(y) + y = self.conv2(y) + if self.stride > 1: + x = self.shortcut(x) + y = y + x + return y + +class PiDiNet(nn.Module): + def __init__(self, inplane, pdcs, dil=None, sa=False, convert=False): + super(PiDiNet, self).__init__() + self.sa = sa + if dil is not None: + assert isinstance(dil, int), 'dil should be an int' + self.dil = dil + + self.fuseplanes = [] + + self.inplane = inplane + if convert: + if pdcs[0] == 'rd': + init_kernel_size = 5 + init_padding = 2 + else: + init_kernel_size = 3 + init_padding = 1 + self.init_block = nn.Conv2d(3, self.inplane, + kernel_size=init_kernel_size, padding=init_padding, bias=False) + block_class = PDCBlock_converted + else: + self.init_block = Conv2d(pdcs[0], 3, self.inplane, kernel_size=3, padding=1) + block_class = PDCBlock + + self.block1_1 = block_class(pdcs[1], self.inplane, self.inplane) + self.block1_2 = block_class(pdcs[2], self.inplane, self.inplane) + self.block1_3 = block_class(pdcs[3], self.inplane, self.inplane) + self.fuseplanes.append(self.inplane) # C + + inplane = self.inplane + self.inplane = self.inplane * 2 + self.block2_1 = block_class(pdcs[4], inplane, self.inplane, stride=2) + self.block2_2 = block_class(pdcs[5], self.inplane, self.inplane) + self.block2_3 = block_class(pdcs[6], self.inplane, self.inplane) + self.block2_4 = block_class(pdcs[7], self.inplane, self.inplane) + self.fuseplanes.append(self.inplane) # 2C + + inplane = self.inplane + self.inplane = self.inplane * 2 + self.block3_1 = block_class(pdcs[8], inplane, self.inplane, stride=2) + self.block3_2 = block_class(pdcs[9], self.inplane, self.inplane) + self.block3_3 = block_class(pdcs[10], self.inplane, self.inplane) + self.block3_4 = block_class(pdcs[11], self.inplane, self.inplane) + self.fuseplanes.append(self.inplane) # 4C + + self.block4_1 = block_class(pdcs[12], self.inplane, self.inplane, stride=2) + self.block4_2 = block_class(pdcs[13], self.inplane, self.inplane) + self.block4_3 = block_class(pdcs[14], self.inplane, self.inplane) + self.block4_4 = block_class(pdcs[15], self.inplane, self.inplane) + self.fuseplanes.append(self.inplane) # 4C + + self.conv_reduces = nn.ModuleList() + if self.sa and self.dil is not None: + self.attentions = nn.ModuleList() + self.dilations = nn.ModuleList() + for i in range(4): + self.dilations.append(CDCM(self.fuseplanes[i], self.dil)) + self.attentions.append(CSAM(self.dil)) + self.conv_reduces.append(MapReduce(self.dil)) + elif self.sa: + self.attentions = nn.ModuleList() + for i in range(4): + self.attentions.append(CSAM(self.fuseplanes[i])) + self.conv_reduces.append(MapReduce(self.fuseplanes[i])) + elif self.dil is not None: + self.dilations = nn.ModuleList() + for i in range(4): + self.dilations.append(CDCM(self.fuseplanes[i], self.dil)) + self.conv_reduces.append(MapReduce(self.dil)) + else: + for i in range(4): + self.conv_reduces.append(MapReduce(self.fuseplanes[i])) + + self.classifier = nn.Conv2d(4, 1, kernel_size=1) # has bias + nn.init.constant_(self.classifier.weight, 0.25) + nn.init.constant_(self.classifier.bias, 0) + + # print('initialization done') + + def get_weights(self): + conv_weights = [] + bn_weights = [] + relu_weights = [] + for pname, p in self.named_parameters(): + if 'bn' in pname: + bn_weights.append(p) + elif 'relu' in pname: + relu_weights.append(p) + else: + conv_weights.append(p) + + return conv_weights, bn_weights, relu_weights + + def forward(self, x): + H, W = x.size()[2:] + + x = self.init_block(x) + + x1 = self.block1_1(x) + x1 = self.block1_2(x1) + x1 = self.block1_3(x1) + + x2 = self.block2_1(x1) + x2 = self.block2_2(x2) + x2 = self.block2_3(x2) + x2 = self.block2_4(x2) + + x3 = self.block3_1(x2) + x3 = self.block3_2(x3) + x3 = self.block3_3(x3) + x3 = self.block3_4(x3) + + x4 = self.block4_1(x3) + x4 = self.block4_2(x4) + x4 = self.block4_3(x4) + x4 = self.block4_4(x4) + + x_fuses = [] + if self.sa and self.dil is not None: + for i, xi in enumerate([x1, x2, x3, x4]): + x_fuses.append(self.attentions[i](self.dilations[i](xi))) + elif self.sa: + for i, xi in enumerate([x1, x2, x3, x4]): + x_fuses.append(self.attentions[i](xi)) + elif self.dil is not None: + for i, xi in enumerate([x1, x2, x3, x4]): + x_fuses.append(self.dilations[i](xi)) + else: + x_fuses = [x1, x2, x3, x4] + + e1 = self.conv_reduces[0](x_fuses[0]) + e1 = F.interpolate(e1, (H, W), mode="bilinear", align_corners=False) + + e2 = self.conv_reduces[1](x_fuses[1]) + e2 = F.interpolate(e2, (H, W), mode="bilinear", align_corners=False) + + e3 = self.conv_reduces[2](x_fuses[2]) + e3 = F.interpolate(e3, (H, W), mode="bilinear", align_corners=False) + + e4 = self.conv_reduces[3](x_fuses[3]) + e4 = F.interpolate(e4, (H, W), mode="bilinear", align_corners=False) + + outputs = [e1, e2, e3, e4] + + output = self.classifier(torch.cat(outputs, dim=1)) + #if not self.training: + # return torch.sigmoid(output) + + outputs.append(output) + outputs = [torch.sigmoid(r) for r in outputs] + return outputs + +def config_model(model): + model_options = list(nets.keys()) + assert model in model_options, \ + 'unrecognized model, please choose from %s' % str(model_options) + + # print(str(nets[model])) + + pdcs = [] + for i in range(16): + layer_name = 'layer%d' % i + op = nets[model][layer_name] + pdcs.append(createConvFunc(op)) + + return pdcs + +def pidinet(): + pdcs = config_model('carv4') + dil = 24 #if args.dil else None + return PiDiNet(60, pdcs, dil=dil, sa=True) + + +if __name__ == '__main__': + model = pidinet() + ckp = torch.load('table5_pidinet.pth')['state_dict'] + model.load_state_dict({k.replace('module.',''):v for k, v in ckp.items()}) + im = cv2.imread('examples/test_my/cat_v4.png') + im = img2tensor(im).unsqueeze(0)/255. + res = model(im)[-1] + res = res>0.5 + res = res.float() + res = (res[0,0].cpu().data.numpy()*255.).astype(np.uint8) + print(res.shape) + cv2.imwrite('edge.png', res) diff --git a/invokeai/backend/image_util/pngwriter.py b/invokeai/backend/image_util/pngwriter.py new file mode 100644 index 00000000000..1f4b42fe217 --- /dev/null +++ b/invokeai/backend/image_util/pngwriter.py @@ -0,0 +1,118 @@ +""" +Two helper classes for dealing with PNG images and their path names. +PngWriter -- Converts Images generated by T2I into PNGs, finds + appropriate names for them, and writes prompt metadata + into the PNG. + +Exports function retrieve_metadata(path) +""" + +import json +import os +import re + +from PIL import Image, PngImagePlugin + +# -------------------image generation utils----- + + +class PngWriter: + def __init__(self, outdir): + self.outdir = outdir + os.makedirs(outdir, exist_ok=True) + + # gives the next unique prefix in outdir + def unique_prefix(self): + # sort reverse alphabetically until we find max+1 + dirlist = sorted(os.listdir(self.outdir), reverse=True) + # find the first filename that matches our pattern or return 000000.0.png + existing_name = next( + (f for f in dirlist if re.match(r"^(\d+)\..*\.png", f)), + "0000000.0.png", + ) + basecount = int(existing_name.split(".", 1)[0]) + 1 + return f"{basecount:06}" + + # saves image named _image_ to outdir/name, writing metadata from prompt + # returns full path of output + def save_image_and_prompt_to_png(self, image, dream_prompt, name, metadata=None, compress_level=6): + path = os.path.join(self.outdir, name) + info = PngImagePlugin.PngInfo() + info.add_text("Dream", dream_prompt) + if metadata: + info.add_text("sd-metadata", json.dumps(metadata)) + image.save(path, "PNG", pnginfo=info, compress_level=compress_level) + return path + + def retrieve_metadata(self, img_basename): + """ + Given a PNG filename stored in outdir, returns the "sd-metadata" + metadata stored there, as a dict + """ + path = os.path.join(self.outdir, img_basename) + all_metadata = retrieve_metadata(path) + return all_metadata["sd-metadata"] + + +def retrieve_metadata(img_path): + """ + Given a path to a PNG image, returns the "sd-metadata" + metadata stored there, as a dict + """ + im = Image.open(img_path) + if hasattr(im, "text"): + md = im.text.get("sd-metadata", "{}") + dream_prompt = im.text.get("Dream", "") + else: + # When trying to retrieve metadata from images without a 'text' payload, such as JPG images. + md = "{}" + dream_prompt = "" + return {"sd-metadata": json.loads(md), "Dream": dream_prompt} + + +def write_metadata(img_path: str, meta: dict): + im = Image.open(img_path) + info = PngImagePlugin.PngInfo() + info.add_text("sd-metadata", json.dumps(meta)) + im.save(img_path, "PNG", pnginfo=info) + + +class PromptFormatter: + def __init__(self, t2i, opt): + self.t2i = t2i + self.opt = opt + + # note: the t2i object should provide all these values. + # there should be no need to or against opt values + def normalize_prompt(self): + """Normalize the prompt and switches""" + t2i = self.t2i + opt = self.opt + + switches = [] + switches.append(f'"{opt.prompt}"') + switches.append(f"-s{opt.steps or t2i.steps}") + switches.append(f"-W{opt.width or t2i.width}") + switches.append(f"-H{opt.height or t2i.height}") + switches.append(f"-C{opt.cfg_scale or t2i.cfg_scale}") + switches.append(f"-A{opt.sampler_name or t2i.sampler_name}") + # to do: put model name into the t2i object + # switches.append(f'--model{t2i.model_name}') + if opt.seamless or t2i.seamless: + switches.append("--seamless") + if opt.init_img: + switches.append(f"-I{opt.init_img}") + if opt.fit: + switches.append("--fit") + if opt.strength and opt.init_img is not None: + switches.append(f"-f{opt.strength or t2i.strength}") + if opt.gfpgan_strength: + switches.append(f"-G{opt.gfpgan_strength}") + if opt.upscale: + switches.append(f"-U {' '.join([str(u) for u in opt.upscale])}") + if opt.variation_amount > 0: + switches.append(f"-v{opt.variation_amount}") + if opt.with_variations: + formatted_variations = ",".join(f"{seed}:{weight}" for seed, weight in opt.with_variations) + switches.append(f"-V{formatted_variations}") + return " ".join(switches) diff --git a/invokeai/backend/image_util/realesrgan/LICENSE b/invokeai/backend/image_util/realesrgan/LICENSE new file mode 100644 index 00000000000..552a1eeaf01 --- /dev/null +++ b/invokeai/backend/image_util/realesrgan/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2021, Xintao Wang +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/invokeai/backend/image_util/realesrgan/__init__.py b/invokeai/backend/image_util/realesrgan/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/image_util/realesrgan/realesrgan.py b/invokeai/backend/image_util/realesrgan/realesrgan.py new file mode 100644 index 00000000000..37853401c21 --- /dev/null +++ b/invokeai/backend/image_util/realesrgan/realesrgan.py @@ -0,0 +1,272 @@ +import math +from enum import Enum +from typing import Any, Optional + +import cv2 +import numpy as np +import numpy.typing as npt +import torch +from cv2.typing import MatLike +from tqdm import tqdm + +from invokeai.backend.image_util.basicsr.rrdbnet_arch import RRDBNet +from invokeai.backend.model_manager.taxonomy import AnyModel +from invokeai.backend.util.devices import TorchDevice + +""" +Adapted from https://github.com/xinntao/Real-ESRGAN/blob/master/realesrgan/utils.py +License is BSD3, copied to `LICENSE` in this directory. + +The adaptation here has a few changes: +- Remove print statements, use `tqdm` to show progress +- Remove unused "outscale" logic, which simply scales the final image to a given factor +- Remove `dni_weight` logic, which was only used when multiple models were used +- Remove logic to fetch models from network +- Add types, rename a few things +""" + + +class ImageMode(str, Enum): + L = "L" + RGB = "RGB" + RGBA = "RGBA" + + +class RealESRGAN: + """A helper class for upsampling images with RealESRGAN. + + Args: + scale (int): Upsampling scale factor used in the networks. It is usually 2 or 4. + model_path (str): The path to the pretrained model. It can be urls (will first download it automatically). + model (nn.Module): The defined network. Default: None. + tile (int): As too large images result in the out of GPU memory issue, so this tile option will first crop + input images into tiles, and then process each of them. Finally, they will be merged into one image. + 0 denotes for do not use tile. Default: 0. + tile_pad (int): The pad size for each tile, to remove border artifacts. Default: 10. + pre_pad (int): Pad the input images to avoid border artifacts. Default: 10. + half (float): Whether to use half precision during inference. Default: False. + """ + + output: torch.Tensor + + def __init__( + self, + scale: int, + loadnet: AnyModel, + model: RRDBNet, + tile: int = 0, + tile_pad: int = 10, + pre_pad: int = 10, + half: bool = False, + ) -> None: + self.scale = scale + self.tile_size = tile + self.tile_pad = tile_pad + self.pre_pad = pre_pad + self.mod_scale: Optional[int] = None + self.half = half + self.device = TorchDevice.choose_torch_device() + + # prefer to use params_ema + if "params_ema" in loadnet: + keyname = "params_ema" + else: + keyname = "params" + + model.load_state_dict(loadnet[keyname], strict=True) + model.eval() + self.model = model.to(self.device) + + if self.half: + self.model = self.model.half() + + def pre_process(self, img: MatLike) -> None: + """Pre-process, such as pre-pad and mod pad, so that the images can be divisible""" + img_tensor: torch.Tensor = torch.from_numpy(np.transpose(img, (2, 0, 1))).float() + self.img = img_tensor.unsqueeze(0).to(self.device) + if self.half: + self.img = self.img.half() + + # pre_pad + if self.pre_pad != 0: + self.img = torch.nn.functional.pad(self.img, (0, self.pre_pad, 0, self.pre_pad), "reflect") + # mod pad for divisible borders + if self.scale == 2: + self.mod_scale = 2 + elif self.scale == 1: + self.mod_scale = 4 + if self.mod_scale is not None: + self.mod_pad_h, self.mod_pad_w = 0, 0 + _, _, h, w = self.img.size() + if h % self.mod_scale != 0: + self.mod_pad_h = self.mod_scale - h % self.mod_scale + if w % self.mod_scale != 0: + self.mod_pad_w = self.mod_scale - w % self.mod_scale + self.img = torch.nn.functional.pad(self.img, (0, self.mod_pad_w, 0, self.mod_pad_h), "reflect") + + def process(self) -> None: + # model inference + self.output = self.model(self.img) + + def tile_process(self) -> None: + """It will first crop input images to tiles, and then process each tile. + Finally, all the processed tiles are merged into one images. + + Modified from: https://github.com/ata4/esrgan-launcher + """ + batch, channel, height, width = self.img.shape + output_height = height * self.scale + output_width = width * self.scale + output_shape = (batch, channel, output_height, output_width) + + # start with black image + self.output = self.img.new_zeros(output_shape) + tiles_x = math.ceil(width / self.tile_size) + tiles_y = math.ceil(height / self.tile_size) + + # loop over all tiles + total_steps = tiles_y * tiles_x + for i in tqdm(range(total_steps), desc="Upscaling"): + y = i // tiles_x + x = i % tiles_x + # extract tile from input image + ofs_x = x * self.tile_size + ofs_y = y * self.tile_size + # input tile area on total image + input_start_x = ofs_x + input_end_x = min(ofs_x + self.tile_size, width) + input_start_y = ofs_y + input_end_y = min(ofs_y + self.tile_size, height) + + # input tile area on total image with padding + input_start_x_pad = max(input_start_x - self.tile_pad, 0) + input_end_x_pad = min(input_end_x + self.tile_pad, width) + input_start_y_pad = max(input_start_y - self.tile_pad, 0) + input_end_y_pad = min(input_end_y + self.tile_pad, height) + + # input tile dimensions + input_tile_width = input_end_x - input_start_x + input_tile_height = input_end_y - input_start_y + input_tile = self.img[ + :, + :, + input_start_y_pad:input_end_y_pad, + input_start_x_pad:input_end_x_pad, + ] + + # upscale tile + with torch.no_grad(): + output_tile = self.model(input_tile) + + # output tile area on total image + output_start_x = input_start_x * self.scale + output_end_x = input_end_x * self.scale + output_start_y = input_start_y * self.scale + output_end_y = input_end_y * self.scale + + # output tile area without padding + output_start_x_tile = (input_start_x - input_start_x_pad) * self.scale + output_end_x_tile = output_start_x_tile + input_tile_width * self.scale + output_start_y_tile = (input_start_y - input_start_y_pad) * self.scale + output_end_y_tile = output_start_y_tile + input_tile_height * self.scale + + # put tile into output image + self.output[:, :, output_start_y:output_end_y, output_start_x:output_end_x] = output_tile[ + :, + :, + output_start_y_tile:output_end_y_tile, + output_start_x_tile:output_end_x_tile, + ] + + def post_process(self) -> torch.Tensor: + # remove extra pad + if self.mod_scale is not None: + _, _, h, w = self.output.size() + self.output = self.output[ + :, + :, + 0 : h - self.mod_pad_h * self.scale, + 0 : w - self.mod_pad_w * self.scale, + ] + # remove prepad + if self.pre_pad != 0: + _, _, h, w = self.output.size() + self.output = self.output[ + :, + :, + 0 : h - self.pre_pad * self.scale, + 0 : w - self.pre_pad * self.scale, + ] + return self.output + + @torch.no_grad() + def upscale(self, img: MatLike, esrgan_alpha_upscale: bool = True) -> npt.NDArray[Any]: + np_img = img.astype(np.float32) + alpha: Optional[np.ndarray] = None + if np.max(np_img) > 256: + # 16-bit image + max_range = 65535 + else: + max_range = 255 + np_img = np_img / max_range + if len(np_img.shape) == 2: + # grayscale image + img_mode = ImageMode.L + np_img = cv2.cvtColor(np_img, cv2.COLOR_GRAY2RGB) + elif np_img.shape[2] == 4: + # RGBA image with alpha channel + img_mode = ImageMode.RGBA + alpha = np_img[:, :, 3] + np_img = np_img[:, :, 0:3] + np_img = cv2.cvtColor(np_img, cv2.COLOR_BGR2RGB) + if esrgan_alpha_upscale: + alpha = cv2.cvtColor(alpha, cv2.COLOR_GRAY2RGB) + else: + img_mode = ImageMode.RGB + np_img = cv2.cvtColor(np_img, cv2.COLOR_BGR2RGB) + + # ------------------- process image (without the alpha channel) ------------------- # + self.pre_process(np_img) + if self.tile_size > 0: + self.tile_process() + else: + self.process() + output_tensor = self.post_process() + output_img: npt.NDArray[Any] = output_tensor.data.squeeze().float().cpu().clamp_(0, 1).numpy() + output_img = np.transpose(output_img[[2, 1, 0], :, :], (1, 2, 0)) + if img_mode is ImageMode.L: + output_img = cv2.cvtColor(output_img, cv2.COLOR_BGR2GRAY) + + # ------------------- process the alpha channel if necessary ------------------- # + if img_mode is ImageMode.RGBA: + if esrgan_alpha_upscale: + assert alpha is not None + self.pre_process(alpha) + if self.tile_size > 0: + self.tile_process() + else: + self.process() + output_alpha_tensor = self.post_process() + output_alpha: npt.NDArray[Any] = output_alpha_tensor.data.squeeze().float().cpu().clamp_(0, 1).numpy() + output_alpha = np.transpose(output_alpha[[2, 1, 0], :, :], (1, 2, 0)) + output_alpha = cv2.cvtColor(output_alpha, cv2.COLOR_BGR2GRAY) + else: # use the cv2 resize for alpha channel + assert alpha is not None + h, w = alpha.shape[0:2] + output_alpha = cv2.resize( + alpha, + (w * self.scale, h * self.scale), + interpolation=cv2.INTER_LINEAR, + ) + + # merge the alpha channel + output_img = cv2.cvtColor(output_img, cv2.COLOR_BGR2BGRA) + output_img[:, :, 3] = output_alpha + + # ------------------------------ return ------------------------------ # + if max_range == 65535: # 16-bit image + output = (output_img * 65535.0).round().astype(np.uint16) + else: + output = (output_img * 255.0).round().astype(np.uint8) + + return output diff --git a/invokeai/backend/image_util/safety_checker.py b/invokeai/backend/image_util/safety_checker.py new file mode 100644 index 00000000000..ab09a296197 --- /dev/null +++ b/invokeai/backend/image_util/safety_checker.py @@ -0,0 +1,84 @@ +""" +This module defines a singleton object, "safety_checker" that +wraps the safety_checker model. It respects the global "nsfw_checker" +configuration variable, that allows the checker to be supressed. +""" + +from pathlib import Path + +import numpy as np +from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from PIL import Image, ImageFilter +from transformers import AutoFeatureExtractor + +import invokeai.backend.util.logging as logger +from invokeai.app.services.config.config_default import get_config +from invokeai.backend.util.devices import TorchDevice +from invokeai.backend.util.silence_warnings import SilenceWarnings + +repo_id = "CompVis/stable-diffusion-safety-checker" +CHECKER_PATH = "core/convert/stable-diffusion-safety-checker" + + +class SafetyChecker: + """ + Wrapper around SafetyChecker model. + """ + + feature_extractor = None + safety_checker = None + + @classmethod + def _load_safety_checker(cls): + if cls.safety_checker is not None and cls.feature_extractor is not None: + return + + try: + model_path = get_config().models_path / CHECKER_PATH + if model_path.exists(): + cls.feature_extractor = AutoFeatureExtractor.from_pretrained(model_path) + cls.safety_checker = StableDiffusionSafetyChecker.from_pretrained(model_path) + else: + model_path.mkdir(parents=True, exist_ok=True) + cls.feature_extractor = AutoFeatureExtractor.from_pretrained(repo_id) + cls.feature_extractor.save_pretrained(model_path, safe_serialization=True) + cls.safety_checker = StableDiffusionSafetyChecker.from_pretrained(repo_id) + cls.safety_checker.save_pretrained(model_path, safe_serialization=True) + except Exception as e: + logger.warning(f"Could not load NSFW checker: {str(e)}") + + @classmethod + def has_nsfw_concept(cls, image: Image.Image) -> bool: + cls._load_safety_checker() + if cls.safety_checker is None or cls.feature_extractor is None: + return False + device = TorchDevice.choose_torch_device() + features = cls.feature_extractor([image], return_tensors="pt") + features.to(device) + cls.safety_checker.to(device) + x_image = np.array(image).astype(np.float32) / 255.0 + x_image = x_image[None].transpose(0, 3, 1, 2) + with SilenceWarnings(): + checked_image, has_nsfw_concept = cls.safety_checker(images=x_image, clip_input=features.pixel_values) + return has_nsfw_concept[0] + + @classmethod + def blur_if_nsfw(cls, image: Image.Image) -> Image.Image: + if cls.has_nsfw_concept(image): + logger.warning("A potentially NSFW image has been detected. Image will be blurred.") + blurry_image = image.filter(filter=ImageFilter.GaussianBlur(radius=32)) + caution = cls._get_caution_img() + # Center the caution image on the blurred image + x = (blurry_image.width - caution.width) // 2 + y = (blurry_image.height - caution.height) // 2 + blurry_image.paste(caution, (x, y), caution) + image = blurry_image + + return image + + @classmethod + def _get_caution_img(cls) -> Image.Image: + import invokeai.app.assets.images as image_assets + + caution = Image.open(Path(image_assets.__path__[0]) / "caution.png") + return caution.resize((caution.width // 2, caution.height // 2)) diff --git a/invokeai/backend/image_util/segment_anything/__init__.py b/invokeai/backend/image_util/segment_anything/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/image_util/segment_anything/mask_refinement.py b/invokeai/backend/image_util/segment_anything/mask_refinement.py new file mode 100644 index 00000000000..2c8cf077d1c --- /dev/null +++ b/invokeai/backend/image_util/segment_anything/mask_refinement.py @@ -0,0 +1,50 @@ +# This file contains utilities for Grounded-SAM mask refinement based on: +# https://github.com/NielsRogge/Transformers-Tutorials/blob/a39f33ac1557b02ebfb191ea7753e332b5ca933f/Grounding%20DINO/GroundingDINO_with_Segment_Anything.ipynb + + +import cv2 +import numpy as np +import numpy.typing as npt + + +def mask_to_polygon(mask: npt.NDArray[np.uint8]) -> list[tuple[int, int]]: + """Convert a binary mask to a polygon. + + Returns: + list[list[int]]: List of (x, y) coordinates representing the vertices of the polygon. + """ + # Find contours in the binary mask. + contours, _ = cv2.findContours(mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + # Find the contour with the largest area. + largest_contour = max(contours, key=cv2.contourArea) + + # Extract the vertices of the contour. + polygon = largest_contour.reshape(-1, 2).tolist() + + return polygon + + +def polygon_to_mask( + polygon: list[tuple[int, int]], image_shape: tuple[int, int], fill_value: int = 1 +) -> npt.NDArray[np.uint8]: + """Convert a polygon to a segmentation mask. + + Args: + polygon (list): List of (x, y) coordinates representing the vertices of the polygon. + image_shape (tuple): Shape of the image (height, width) for the mask. + fill_value (int): Value to fill the polygon with. + + Returns: + np.ndarray: Segmentation mask with the polygon filled (with value 255). + """ + # Create an empty mask. + mask = np.zeros(image_shape, dtype=np.uint8) + + # Convert polygon to an array of points. + pts = np.array(polygon, dtype=np.int32) + + # Fill the polygon with white color (255). + cv2.fillPoly(mask, [pts], color=(fill_value,)) + + return mask diff --git a/invokeai/backend/image_util/segment_anything/segment_anything_2_pipeline.py b/invokeai/backend/image_util/segment_anything/segment_anything_2_pipeline.py new file mode 100644 index 00000000000..79a7d91a7bf --- /dev/null +++ b/invokeai/backend/image_util/segment_anything/segment_anything_2_pipeline.py @@ -0,0 +1,109 @@ +from typing import Optional + +import torch +from PIL import Image + +# Import SAM2 components - these should be available in transformers 4.56.0+ +from transformers.models.sam2 import Sam2Model +from transformers.models.sam2.processing_sam2 import Sam2Processor + +from invokeai.backend.image_util.segment_anything.shared import SAMInput +from invokeai.backend.raw_model import RawModel + + +class SegmentAnything2Pipeline(RawModel): + """A wrapper class for the transformers SAM2 model and processor that makes it compatible with the model manager.""" + + def __init__(self, sam2_model: Sam2Model, sam2_processor: Sam2Processor): + """Initialize the SAM2 pipeline. + + Args: + sam2_model: The SAM2 model + sam2_processor: The SAM2 processor (can be Sam2Processor or Sam2VideoProcessor) + """ + self._sam2_model = sam2_model + self._sam2_processor = sam2_processor + + def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None): + # HACK: The SAM2 pipeline may not work on MPS devices. We only allow it to be moved to CPU or CUDA. + if device is not None and device.type not in {"cpu", "cuda"}: + device = None + self._sam2_model.to(device=device, dtype=dtype) + + def calc_size(self) -> int: + # HACK: Fix the circular import issue. + from invokeai.backend.model_manager.load.model_util import calc_module_size + + return calc_module_size(self._sam2_model) + + def segment( + self, + image: Image.Image, + inputs: list[SAMInput], + ) -> torch.Tensor: + """Segment the image using the provided inputs. + + Args: + image: The image to segment. + inputs: A list of SAMInput objects containing bounding boxes and/or point lists. + + Returns: + torch.Tensor: The segmentation masks. dtype: torch.bool. shape: [num_masks, channels, height, width]. + """ + + input_boxes: list[list[float]] = [] + input_points: list[list[list[float]]] = [] + input_labels: list[list[int]] = [] + + for i in inputs: + box: list[float] | None = None + points: list[list[float]] | None = None + labels: list[int] | None = None + + if i.bounding_box is not None: + box: list[float] | None = [ + i.bounding_box.x_min, + i.bounding_box.y_min, + i.bounding_box.x_max, + i.bounding_box.y_max, + ] + + if i.points is not None: + points = [] + labels = [] + for point in i.points: + points.append([point.x, point.y]) + labels.append(point.label.value) + + if box is not None: + input_boxes.append(box) + if points is not None: + input_points.append(points) + if labels is not None: + input_labels.append(labels) + + batched_input_boxes = [input_boxes] if input_boxes else None + batched_input_points = [input_points] if input_points else None + batched_input_labels = [input_labels] if input_labels else None + + processed_inputs = self._sam2_processor( + images=image, + input_boxes=batched_input_boxes, + input_points=batched_input_points, + input_labels=batched_input_labels, + return_tensors="pt", + ).to(self._sam2_model.device) + + # Generate masks using the SAM2 model + outputs = self._sam2_model(**processed_inputs) + + # Post-process the masks to get the final segmentation + masks = self._sam2_processor.post_process_masks( + masks=outputs.pred_masks, + original_sizes=processed_inputs.original_sizes, + reshaped_input_sizes=processed_inputs.reshaped_input_sizes, + ) + + # There should be only one batch. + assert len(masks) == 1 + return masks[0] diff --git a/invokeai/backend/image_util/segment_anything/segment_anything_pipeline.py b/invokeai/backend/image_util/segment_anything/segment_anything_pipeline.py new file mode 100644 index 00000000000..f33702186f5 --- /dev/null +++ b/invokeai/backend/image_util/segment_anything/segment_anything_pipeline.py @@ -0,0 +1,97 @@ +from typing import Optional + +import torch +from PIL import Image +from transformers.models.sam import SamModel +from transformers.models.sam.processing_sam import SamProcessor + +from invokeai.backend.image_util.segment_anything.shared import SAMInput +from invokeai.backend.raw_model import RawModel + + +class SegmentAnythingPipeline(RawModel): + """A wrapper class for the transformers SAM model and processor that makes it compatible with the model manager.""" + + def __init__(self, sam_model: SamModel, sam_processor: SamProcessor): + self._sam_model = sam_model + self._sam_processor = sam_processor + + def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None): + # HACK(ryand): The SAM pipeline does not work on MPS devices. We only allow it to be moved to CPU or CUDA. + if device is not None and device.type not in {"cpu", "cuda"}: + device = None + self._sam_model.to(device=device, dtype=dtype) + + def calc_size(self) -> int: + # HACK(ryand): Fix the circular import issue. + from invokeai.backend.model_manager.load.model_util import calc_module_size + + return calc_module_size(self._sam_model) + + def segment( + self, + image: Image.Image, + inputs: list[SAMInput], + ) -> torch.Tensor: + """Segment the image using the provided inputs. + + Args: + image: The image to segment. + inputs: A list of SAMInput objects containing bounding boxes and/or point lists. + + Returns: + torch.Tensor: The segmentation masks. dtype: torch.bool. shape: [num_masks, channels, height, width]. + """ + + input_boxes: list[list[float]] = [] + input_points: list[list[list[float]]] = [] + input_labels: list[list[int]] = [] + + for i in inputs: + box: list[float] | None = None + points: list[list[float]] | None = None + labels: list[int] | None = None + + if i.bounding_box is not None: + box: list[float] | None = [ + i.bounding_box.x_min, + i.bounding_box.y_min, + i.bounding_box.x_max, + i.bounding_box.y_max, + ] + + if i.points is not None: + points = [] + labels = [] + for point in i.points: + points.append([point.x, point.y]) + labels.append(point.label.value) + + if box is not None: + input_boxes.append(box) + if points is not None: + input_points.append(points) + if labels is not None: + input_labels.append(labels) + + batched_input_boxes = [input_boxes] if input_boxes else None + batched_input_points = input_points if input_points else None + batched_input_labels = input_labels if input_labels else None + + processed_inputs = self._sam_processor( + images=image, + input_boxes=batched_input_boxes, + input_points=batched_input_points, + input_labels=batched_input_labels, + return_tensors="pt", + ).to(self._sam_model.device) + outputs = self._sam_model(**processed_inputs) + masks = self._sam_processor.post_process_masks( + masks=outputs.pred_masks, + original_sizes=processed_inputs.original_sizes, + reshaped_input_sizes=processed_inputs.reshaped_input_sizes, + ) + + # There should be only one batch. + assert len(masks) == 1 + return masks[0] diff --git a/invokeai/backend/image_util/segment_anything/shared.py b/invokeai/backend/image_util/segment_anything/shared.py new file mode 100644 index 00000000000..240a8ecc25d --- /dev/null +++ b/invokeai/backend/image_util/segment_anything/shared.py @@ -0,0 +1,49 @@ +from enum import Enum + +from pydantic import BaseModel, model_validator +from pydantic.fields import Field + + +class BoundingBox(BaseModel): + x_min: int = Field(..., description="The minimum x-coordinate of the bounding box (inclusive).") + x_max: int = Field(..., description="The maximum x-coordinate of the bounding box (exclusive).") + y_min: int = Field(..., description="The minimum y-coordinate of the bounding box (inclusive).") + y_max: int = Field(..., description="The maximum y-coordinate of the bounding box (exclusive).") + + @model_validator(mode="after") + def check_coords(self): + if self.x_min > self.x_max: + raise ValueError(f"x_min ({self.x_min}) is greater than x_max ({self.x_max}).") + if self.y_min > self.y_max: + raise ValueError(f"y_min ({self.y_min}) is greater than y_max ({self.y_max}).") + return self + + def tuple(self) -> tuple[int, int, int, int]: + """ + Returns the bounding box as a tuple suitable for use with PIL's `Image.crop()` method. + This method returns a tuple of the form (left, upper, right, lower) == (x_min, y_min, x_max, y_max). + """ + return (self.x_min, self.y_min, self.x_max, self.y_max) + + +class SAMPointLabel(Enum): + negative = -1 + neutral = 0 + positive = 1 + + +class SAMPoint(BaseModel): + x: int = Field(..., description="The x-coordinate of the point") + y: int = Field(..., description="The y-coordinate of the point") + label: SAMPointLabel = Field(..., description="The label of the point") + + +class SAMInput(BaseModel): + bounding_box: BoundingBox | None = Field(None, description="The bounding box to use for segmentation") + points: list[SAMPoint] | None = Field(None, description="The points to use for segmentation") + + @model_validator(mode="after") + def check_input(self): + if not self.bounding_box and not self.points: + raise ValueError("Either bounding_box or points must be provided") + return self diff --git a/invokeai/backend/image_util/util.py b/invokeai/backend/image_util/util.py new file mode 100644 index 00000000000..1e7aad4eb45 --- /dev/null +++ b/invokeai/backend/image_util/util.py @@ -0,0 +1,247 @@ +from math import ceil, floor, sqrt +from typing import Optional + +import cv2 +import numpy as np +from PIL import Image + + +class InitImageResizer: + """Simple class to create resized copies of an Image while preserving the aspect ratio.""" + + def __init__(self, Image): + self.image = Image + + def resize(self, width=None, height=None) -> Image.Image: + """ + Return a copy of the image resized to fit within + a box width x height. The aspect ratio is + maintained. If neither width nor height are provided, + then returns a copy of the original image. If one or the other is + provided, then the other will be calculated from the + aspect ratio. + + Everything is floored to the nearest multiple of 64 so + that it can be passed to img2img() + """ + im = self.image + + ar = im.width / float(im.height) + + # Infer missing values from aspect ratio + if not (width or height): # both missing + width = im.width + height = im.height + elif not height: # height missing + height = int(width / ar) + elif not width: # width missing + width = int(height * ar) + + w_scale = width / im.width + h_scale = height / im.height + scale = min(w_scale, h_scale) + (rw, rh) = (int(scale * im.width), int(scale * im.height)) + + # round everything to multiples of 64 + width, height, rw, rh = (x - x % 64 for x in (width, height, rw, rh)) + + # no resize necessary, but return a copy + if im.width == width and im.height == height: + return im.copy() + + # otherwise resize the original image so that it fits inside the bounding box + resized_image = self.image.resize((rw, rh), resample=Image.Resampling.LANCZOS) + return resized_image + + +def make_grid(image_list, rows=None, cols=None): + image_cnt = len(image_list) + if None in (rows, cols): + rows = floor(sqrt(image_cnt)) # try to make it square + cols = ceil(image_cnt / rows) + width = image_list[0].width + height = image_list[0].height + + grid_img = Image.new("RGB", (width * cols, height * rows)) + i = 0 + for r in range(0, rows): + for c in range(0, cols): + if i >= len(image_list): + break + grid_img.paste(image_list[i], (c * width, r * height)) + i = i + 1 + + return grid_img + + +def pil_to_np(image: Image.Image) -> np.ndarray: + """Converts a PIL image to a numpy array.""" + return np.array(image, dtype=np.uint8) + + +def np_to_pil(image: np.ndarray) -> Image.Image: + """Converts a numpy array to a PIL image.""" + return Image.fromarray(image) + + +def pil_to_cv2(image: Image.Image) -> np.ndarray: + """Converts a PIL image to a CV2 image.""" + + if image.mode == "RGBA": + return cv2.cvtColor(np.array(image, dtype=np.uint8), cv2.COLOR_RGBA2BGRA) + else: + return cv2.cvtColor(np.array(image, dtype=np.uint8), cv2.COLOR_RGB2BGR) + + +def cv2_to_pil(image: np.ndarray) -> Image.Image: + """Converts a CV2 image to a PIL image.""" + + if image.ndim == 3 and image.shape[2] == 4: + return Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGRA2RGBA)) + else: + return Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) + + +def normalize_image_channel_count(image: np.ndarray) -> np.ndarray: + """Normalizes an image to have 3 channels. + + If the image has 1 channel, it will be duplicated 3 times. + If the image has 1 channel, a third empty channel will be added. + If the image has 4 channels, the alpha channel will be used to blend the image with a white background. + + Adapted from https://github.com/huggingface/controlnet_aux (Apache-2.0 license). + + Args: + image: The input image. + + Returns: + The normalized image. + """ + assert image.dtype == np.uint8 + if image.ndim == 2: + image = image[:, :, None] + assert image.ndim == 3 + _height, _width, channels = image.shape + assert channels == 1 or channels == 3 or channels == 4 + if channels == 3: + return image + if channels == 1: + return np.concatenate([image, image, image], axis=2) + if channels == 4: + color = image[:, :, 0:3].astype(np.float32) + alpha = image[:, :, 3:4].astype(np.float32) / 255.0 + normalized = color * alpha + 255.0 * (1.0 - alpha) + normalized = normalized.clip(0, 255).astype(np.uint8) + return normalized + + raise ValueError("Invalid number of channels.") + + +def resize_image_to_resolution(input_image: np.ndarray, resolution: int) -> np.ndarray: + """Resizes an image, fitting it to the given resolution. + + Adapted from https://github.com/huggingface/controlnet_aux (Apache-2.0 license). + + Args: + input_image: The input image. + resolution: The resolution to fit the image to. + + Returns: + The resized image. + """ + h = float(input_image.shape[0]) + w = float(input_image.shape[1]) + scaling_factor = float(resolution) / min(h, w) + h = int(h * scaling_factor) + w = int(w * scaling_factor) + if scaling_factor > 1: + return cv2.resize(input_image, (w, h), interpolation=cv2.INTER_LANCZOS4) + else: + return cv2.resize(input_image, (w, h), interpolation=cv2.INTER_AREA) + + +def nms(np_img: np.ndarray, threshold: Optional[int] = None, sigma: Optional[float] = None) -> np.ndarray: + """ + Apply non-maximum suppression to an image. + + If both threshold and sigma are provided, the image will blurred before the suppression and thresholded afterwards, + resulting in a binary output image. + + This function is adapted from https://github.com/lllyasviel/ControlNet. + + Args: + image: The input image. + threshold: The threshold value for the suppression. Pixels with values greater than this will be set to 255. + sigma: The standard deviation for the Gaussian blur applied to the image. + + Returns: + The image after non-maximum suppression. + + Raises: + ValueError: If only one of threshold and sigma provided. + """ + + # Raise a value error if only one of threshold and sigma is provided + if (threshold is None) != (sigma is None): + raise ValueError("Both threshold and sigma must be provided if one is provided.") + + if sigma is not None and threshold is not None: + # Blurring the image can help to thin out features + np_img = cv2.GaussianBlur(np_img.astype(np.float32), (0, 0), sigma) + + filter_1 = np.array([[0, 0, 0], [1, 1, 1], [0, 0, 0]], dtype=np.uint8) + filter_2 = np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8) + filter_3 = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=np.uint8) + filter_4 = np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]], dtype=np.uint8) + + nms_img = np.zeros_like(np_img) + + for f in [filter_1, filter_2, filter_3, filter_4]: + np.putmask(nms_img, cv2.dilate(np_img, kernel=f) == np_img, np_img) + + if sigma is not None and threshold is not None: + # We blurred - now threshold to get a binary image + thresholded = np.zeros_like(nms_img, dtype=np.uint8) + thresholded[nms_img > threshold] = 255 + return thresholded + + return nms_img + + +def safe_step(x: np.ndarray, step: int = 2) -> np.ndarray: + """Apply the safe step operation to an array. + + I don't fully understand the purpose of this function, but it appears to be normalizing/quantizing the array. + + Adapted from https://github.com/huggingface/controlnet_aux (Apache-2.0 license). + + Args: + x: The input array. + step: The step value. + + Returns: + The array after the safe step operation. + """ + y = x.astype(np.float32) * float(step + 1) + y = y.astype(np.int32).astype(np.float32) / float(step) + return y + + +def resize_to_multiple(image: np.ndarray, multiple: int) -> np.ndarray: + """Resize an image to make its dimensions multiples of the given number.""" + + # Get the original dimensions + height, width = image.shape[:2] + + # Calculate the scaling factor to make the dimensions multiples of the given number + new_width = (width // multiple) * multiple + new_height = int((new_width / width) * height) + + # If new_height is not a multiple, adjust it + if new_height % multiple != 0: + new_height = (new_height // multiple) * multiple + + # Resize the image + resized_image = cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_AREA) + + return resized_image diff --git a/invokeai/backend/ip_adapter/README.md b/invokeai/backend/ip_adapter/README.md new file mode 100644 index 00000000000..7ac845e5346 --- /dev/null +++ b/invokeai/backend/ip_adapter/README.md @@ -0,0 +1,46 @@ +# IP-Adapter Model Formats + +The official IP-Adapter models are released here: [h94/IP-Adapter](https://huggingface.co/h94/IP-Adapter) + +This official model repo does not integrate well with InvokeAI's current approach to model management, so we have defined a new file structure for IP-Adapter models. The InvokeAI format is described below. + +## CLIP Vision Models + +CLIP Vision models are organized in `diffusers`` format. The expected directory structure is: + +```bash +ip_adapter_sd_image_encoder/ +├── config.json +└── model.safetensors +``` + +## IP-Adapter Models + +IP-Adapter models are stored in a directory containing two files +- `image_encoder.txt`: A text file containing the model identifier for the CLIP Vision encoder that is intended to be used with this IP-Adapter model. +- `ip_adapter.bin`: The IP-Adapter weights. + +Sample directory structure: +```bash +ip_adapter_sd15/ +├── image_encoder.txt +└── ip_adapter.bin +``` + +### Why save the weights in a .safetensors file? + +The weights in `ip_adapter.bin` are stored in a nested dict, which is not supported by `safetensors`. This could be solved by splitting `ip_adapter.bin` into multiple files, but for now we have decided to maintain consistency with the checkpoint structure used in the official [h94/IP-Adapter](https://huggingface.co/h94/IP-Adapter) repo. + +## InvokeAI Hosted IP-Adapters + +Image Encoders: +- [InvokeAI/ip_adapter_sd_image_encoder](https://huggingface.co/InvokeAI/ip_adapter_sd_image_encoder) +- [InvokeAI/ip_adapter_sdxl_image_encoder](https://huggingface.co/InvokeAI/ip_adapter_sdxl_image_encoder) + +IP-Adapters: +- [InvokeAI/ip_adapter_sd15](https://huggingface.co/InvokeAI/ip_adapter_sd15) +- [InvokeAI/ip_adapter_plus_sd15](https://huggingface.co/InvokeAI/ip_adapter_plus_sd15) +- [InvokeAI/ip_adapter_plus_face_sd15](https://huggingface.co/InvokeAI/ip_adapter_plus_face_sd15) +- [InvokeAI/ip_adapter_sdxl](https://huggingface.co/InvokeAI/ip_adapter_sdxl) +- [InvokeAI/ip_adapter_sdxl_vit_h](https://huggingface.co/InvokeAI/ip_adapter_sdxl_vit_h) +- [InvokeAI/ip-adapter-plus_sdxl_vit-h](https://huggingface.co/InvokeAI/ip-adapter-plus_sdxl_vit-h) \ No newline at end of file diff --git a/invokeai/backend/ip_adapter/__init__.py b/invokeai/backend/ip_adapter/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/ip_adapter/ip_adapter.py b/invokeai/backend/ip_adapter/ip_adapter.py new file mode 100644 index 00000000000..4607ddd0120 --- /dev/null +++ b/invokeai/backend/ip_adapter/ip_adapter.py @@ -0,0 +1,262 @@ +# copied from https://github.com/tencent-ailab/IP-Adapter (Apache License 2.0) +# and modified as needed + +import pathlib +from typing import List, Optional, TypedDict, Union + +import safetensors +import safetensors.torch +import torch +from PIL import Image +from transformers import CLIPImageProcessor, CLIPVisionModelWithProjection + +from invokeai.backend.ip_adapter.ip_attention_weights import IPAttentionWeights +from invokeai.backend.ip_adapter.resampler import Resampler +from invokeai.backend.raw_model import RawModel + + +class IPAdapterStateDict(TypedDict): + ip_adapter: dict[str, torch.Tensor] + image_proj: dict[str, torch.Tensor] + + +class ImageProjModel(torch.nn.Module): + """Image Projection Model""" + + def __init__( + self, cross_attention_dim: int = 1024, clip_embeddings_dim: int = 1024, clip_extra_context_tokens: int = 4 + ): + super().__init__() + + self.cross_attention_dim = cross_attention_dim + self.clip_extra_context_tokens = clip_extra_context_tokens + self.proj = torch.nn.Linear(clip_embeddings_dim, self.clip_extra_context_tokens * cross_attention_dim) + self.norm = torch.nn.LayerNorm(cross_attention_dim) + + @classmethod + def from_state_dict(cls, state_dict: dict[str, torch.Tensor], clip_extra_context_tokens: int = 4): + """Initialize an ImageProjModel from a state_dict. + + The cross_attention_dim and clip_embeddings_dim are inferred from the shape of the tensors in the state_dict. + + Args: + state_dict (dict[torch.Tensor]): The state_dict of model weights. + clip_extra_context_tokens (int, optional): Defaults to 4. + + Returns: + ImageProjModel + """ + cross_attention_dim = state_dict["norm.weight"].shape[0] + clip_embeddings_dim = state_dict["proj.weight"].shape[-1] + + model = cls(cross_attention_dim, clip_embeddings_dim, clip_extra_context_tokens) + + model.load_state_dict(state_dict) + return model + + def forward(self, image_embeds: torch.Tensor): + embeds = image_embeds + clip_extra_context_tokens = self.proj(embeds).reshape( + -1, self.clip_extra_context_tokens, self.cross_attention_dim + ) + clip_extra_context_tokens = self.norm(clip_extra_context_tokens) + return clip_extra_context_tokens + + +class MLPProjModel(torch.nn.Module): + """SD model with image prompt""" + + def __init__(self, cross_attention_dim: int = 1024, clip_embeddings_dim: int = 1024): + super().__init__() + + self.proj = torch.nn.Sequential( + torch.nn.Linear(clip_embeddings_dim, clip_embeddings_dim), + torch.nn.GELU(), + torch.nn.Linear(clip_embeddings_dim, cross_attention_dim), + torch.nn.LayerNorm(cross_attention_dim), + ) + + @classmethod + def from_state_dict(cls, state_dict: dict[str, torch.Tensor]): + """Initialize an MLPProjModel from a state_dict. + + The cross_attention_dim and clip_embeddings_dim are inferred from the shape of the tensors in the state_dict. + + Args: + state_dict (dict[torch.Tensor]): The state_dict of model weights. + + Returns: + MLPProjModel + """ + cross_attention_dim = state_dict["proj.3.weight"].shape[0] + clip_embeddings_dim = state_dict["proj.0.weight"].shape[0] + + model = cls(cross_attention_dim, clip_embeddings_dim) + + model.load_state_dict(state_dict) + return model + + def forward(self, image_embeds: torch.Tensor): + clip_extra_context_tokens = self.proj(image_embeds) + return clip_extra_context_tokens + + +class IPAdapter(RawModel): + """IP-Adapter: https://arxiv.org/pdf/2308.06721.pdf""" + + def __init__( + self, + state_dict: IPAdapterStateDict, + device: torch.device, + dtype: torch.dtype = torch.float16, + num_tokens: int = 4, + ): + self.device = device + self.dtype = dtype + + self._num_tokens = num_tokens + + self._clip_image_processor = CLIPImageProcessor() + + self._image_proj_model = self._init_image_proj_model(state_dict["image_proj"]) + + self.attn_weights = IPAttentionWeights.from_state_dict(state_dict["ip_adapter"]).to( + self.device, dtype=self.dtype + ) + + def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None): + if device is not None: + self.device = device + if dtype is not None: + self.dtype = dtype + + self._image_proj_model.to(device=self.device, dtype=self.dtype) + self.attn_weights.to(device=self.device, dtype=self.dtype) + + def calc_size(self) -> int: + # HACK(ryand): Fix this issue with circular imports. + from invokeai.backend.model_manager.load.model_util import calc_module_size + + return calc_module_size(self._image_proj_model) + calc_module_size(self.attn_weights) + + def _init_image_proj_model( + self, state_dict: dict[str, torch.Tensor] + ) -> Union[ImageProjModel, Resampler, MLPProjModel]: + return ImageProjModel.from_state_dict(state_dict, self._num_tokens).to(self.device, dtype=self.dtype) + + @torch.inference_mode() + def get_image_embeds(self, pil_image: List[Image.Image], image_encoder: CLIPVisionModelWithProjection): + clip_image = self._clip_image_processor(images=pil_image, return_tensors="pt").pixel_values + clip_image_embeds = image_encoder(clip_image.to(self.device, dtype=self.dtype)).image_embeds + try: + image_prompt_embeds = self._image_proj_model(clip_image_embeds) + uncond_image_prompt_embeds = self._image_proj_model(torch.zeros_like(clip_image_embeds)) + return image_prompt_embeds, uncond_image_prompt_embeds + except RuntimeError as e: + raise RuntimeError("Selected CLIP Vision Model is incompatible with the current IP Adapter") from e + + +class IPAdapterPlus(IPAdapter): + """IP-Adapter with fine-grained features""" + + def _init_image_proj_model(self, state_dict: dict[str, torch.Tensor]) -> Union[Resampler, MLPProjModel]: + return Resampler.from_state_dict( + state_dict=state_dict, + depth=4, + dim_head=64, + heads=12, + num_queries=self._num_tokens, + ff_mult=4, + ).to(self.device, dtype=self.dtype) + + @torch.inference_mode() + def get_image_embeds(self, pil_image: List[Image.Image], image_encoder: CLIPVisionModelWithProjection): + clip_image = self._clip_image_processor(images=pil_image, return_tensors="pt").pixel_values + clip_image = clip_image.to(self.device, dtype=self.dtype) + clip_image_embeds = image_encoder(clip_image, output_hidden_states=True).hidden_states[-2] + uncond_clip_image_embeds = image_encoder(torch.zeros_like(clip_image), output_hidden_states=True).hidden_states[ + -2 + ] + try: + image_prompt_embeds = self._image_proj_model(clip_image_embeds) + uncond_image_prompt_embeds = self._image_proj_model(uncond_clip_image_embeds) + return image_prompt_embeds, uncond_image_prompt_embeds + except RuntimeError as e: + raise RuntimeError("Selected CLIP Vision Model is incompatible with the current IP Adapter") from e + + +class IPAdapterFull(IPAdapterPlus): + """IP-Adapter Plus with full features.""" + + def _init_image_proj_model(self, state_dict: dict[str, torch.Tensor]): + return MLPProjModel.from_state_dict(state_dict).to(self.device, dtype=self.dtype) + + +class IPAdapterPlusXL(IPAdapterPlus): + """IP-Adapter Plus for SDXL.""" + + def _init_image_proj_model(self, state_dict: dict[str, torch.Tensor]): + return Resampler.from_state_dict( + state_dict=state_dict, + depth=4, + dim_head=64, + heads=20, + num_queries=self._num_tokens, + ff_mult=4, + ).to(self.device, dtype=self.dtype) + + +def load_ip_adapter_tensors(ip_adapter_ckpt_path: pathlib.Path, device: str) -> IPAdapterStateDict: + state_dict: IPAdapterStateDict = { + "ip_adapter": {}, + "image_proj": {}, + "adapter_modules": {}, # added for noobai-mark-ipa + "image_proj_model": {}, # added for noobai-mark-ipa + } + + if ip_adapter_ckpt_path.suffix == ".safetensors": + model = safetensors.torch.load_file(ip_adapter_ckpt_path, device=device) + for key in model.keys(): + if key.startswith("ip_adapter."): + state_dict["ip_adapter"][key.replace("ip_adapter.", "")] = model[key] + elif key.startswith("image_proj_model."): + state_dict["image_proj_model"][key.replace("image_proj_model.", "")] = model[key] + elif key.startswith("image_proj."): + state_dict["image_proj"][key.replace("image_proj.", "")] = model[key] + elif key.startswith("adapter_modules."): + state_dict["adapter_modules"][key.replace("adapter_modules.", "")] = model[key] + else: + raise RuntimeError(f"Encountered unexpected IP Adapter state dict key: '{key}'.") + else: + ip_adapter_diffusers_checkpoint_path = ip_adapter_ckpt_path / "ip_adapter.bin" + state_dict = torch.load(ip_adapter_diffusers_checkpoint_path, map_location="cpu") + + return state_dict + + +def build_ip_adapter( + ip_adapter_ckpt_path: pathlib.Path, device: torch.device, dtype: torch.dtype = torch.float16 +) -> Union[IPAdapter, IPAdapterPlus, IPAdapterPlusXL, IPAdapterPlus]: + state_dict = load_ip_adapter_tensors(ip_adapter_ckpt_path, device.type) + + # IPAdapter (with ImageProjModel) + if "proj.weight" in state_dict["image_proj"]: + return IPAdapter(state_dict, device=device, dtype=dtype) + + # IPAdaterPlus or IPAdapterPlusXL (with Resampler) + elif "proj_in.weight" in state_dict["image_proj"]: + cross_attention_dim = state_dict["ip_adapter"]["1.to_k_ip.weight"].shape[-1] + if cross_attention_dim == 768: + return IPAdapterPlus(state_dict, device=device, dtype=dtype) # SD1 IP-Adapter Plus + elif cross_attention_dim == 2048: + return IPAdapterPlusXL(state_dict, device=device, dtype=dtype) # SDXL IP-Adapter Plus + else: + raise Exception(f"Unsupported IP-Adapter Plus cross-attention dimension: {cross_attention_dim}.") + + # IPAdapterFull (with MLPProjModel) + elif "proj.0.weight" in state_dict["image_proj"]: + return IPAdapterFull(state_dict, device=device, dtype=dtype) + + # Unrecognized IP Adapter Architectures + else: + raise ValueError(f"'{ip_adapter_ckpt_path}' has an unrecognized IP-Adapter model architecture.") diff --git a/invokeai/backend/ip_adapter/ip_attention_weights.py b/invokeai/backend/ip_adapter/ip_attention_weights.py new file mode 100644 index 00000000000..9c3b8969c68 --- /dev/null +++ b/invokeai/backend/ip_adapter/ip_attention_weights.py @@ -0,0 +1,46 @@ +import torch + + +class IPAttentionProcessorWeights(torch.nn.Module): + """The IP-Adapter weights for a single attention processor. + + This class is a torch.nn.Module sub-class to facilitate loading from a state_dict. It does not have a forward(...) + method. + """ + + def __init__(self, in_dim: int, out_dim: int): + super().__init__() + self.to_k_ip = torch.nn.Linear(in_dim, out_dim, bias=False) + self.to_v_ip = torch.nn.Linear(in_dim, out_dim, bias=False) + + +class IPAttentionWeights(torch.nn.Module): + """A collection of all the `IPAttentionProcessorWeights` objects for an IP-Adapter model. + + This class is a torch.nn.Module sub-class so that it inherits the `.to(...)` functionality. It does not have a + forward(...) method. + """ + + def __init__(self, weights: torch.nn.ModuleDict): + super().__init__() + self._weights = weights + + def get_attention_processor_weights(self, idx: int) -> IPAttentionProcessorWeights: + """Get the `IPAttentionProcessorWeights` for the idx'th attention processor.""" + # Cast to int first, because we expect the key to represent an int. Then cast back to str, because + # `torch.nn.ModuleDict` only supports str keys. + return self._weights[str(int(idx))] + + @classmethod + def from_state_dict(cls, state_dict: dict[str, torch.Tensor]): + attn_proc_weights: dict[str, IPAttentionProcessorWeights] = {} + + for tensor_name, tensor in state_dict.items(): + if "to_k_ip.weight" in tensor_name: + index = str(int(tensor_name.split(".")[0])) + attn_proc_weights[index] = IPAttentionProcessorWeights(tensor.shape[1], tensor.shape[0]) + + attn_proc_weights_module = torch.nn.ModuleDict(attn_proc_weights) + attn_proc_weights_module.load_state_dict(state_dict) + + return cls(attn_proc_weights_module) diff --git a/invokeai/backend/ip_adapter/resampler.py b/invokeai/backend/ip_adapter/resampler.py new file mode 100644 index 00000000000..a32eeacfdc2 --- /dev/null +++ b/invokeai/backend/ip_adapter/resampler.py @@ -0,0 +1,166 @@ +# copied from https://github.com/tencent-ailab/IP-Adapter (Apache License 2.0) + +# tencent ailab comment: modified from +# https://github.com/mlfoundations/open_flamingo/blob/main/open_flamingo/src/helpers.py +import math + +import torch +import torch.nn as nn + + +# FFN +def FeedForward(dim: int, mult: int = 4): + inner_dim = dim * mult + return nn.Sequential( + nn.LayerNorm(dim), + nn.Linear(dim, inner_dim, bias=False), + nn.GELU(), + nn.Linear(inner_dim, dim, bias=False), + ) + + +def reshape_tensor(x: torch.Tensor, heads: int): + bs, length, _ = x.shape + # (bs, length, width) --> (bs, length, n_heads, dim_per_head) + x = x.view(bs, length, heads, -1) + # (bs, length, n_heads, dim_per_head) --> (bs, n_heads, length, dim_per_head) + x = x.transpose(1, 2) + # (bs, n_heads, length, dim_per_head) --> (bs*n_heads, length, dim_per_head) + x = x.reshape(bs, heads, length, -1) + return x + + +class PerceiverAttention(nn.Module): + def __init__(self, *, dim: int, dim_head: int = 64, heads: int = 8): + super().__init__() + self.scale = dim_head**-0.5 + self.dim_head = dim_head + self.heads = heads + inner_dim = dim_head * heads + + self.norm1 = nn.LayerNorm(dim) + self.norm2 = nn.LayerNorm(dim) + + self.to_q = nn.Linear(dim, inner_dim, bias=False) + self.to_kv = nn.Linear(dim, inner_dim * 2, bias=False) + self.to_out = nn.Linear(inner_dim, dim, bias=False) + + def forward(self, x: torch.Tensor, latents: torch.Tensor): + """ + Args: + x (torch.Tensor): image features + shape (b, n1, D) + latent (torch.Tensor): latent features + shape (b, n2, D) + """ + x = self.norm1(x) + latents = self.norm2(latents) + + b, L, _ = latents.shape + + q = self.to_q(latents) + kv_input = torch.cat((x, latents), dim=-2) + k, v = self.to_kv(kv_input).chunk(2, dim=-1) + + q = reshape_tensor(q, self.heads) + k = reshape_tensor(k, self.heads) + v = reshape_tensor(v, self.heads) + + # attention + scale = 1 / math.sqrt(math.sqrt(self.dim_head)) + weight = (q * scale) @ (k * scale).transpose(-2, -1) # More stable with f16 than dividing afterwards + weight = torch.softmax(weight.float(), dim=-1).type(weight.dtype) + out = weight @ v + + out = out.permute(0, 2, 1, 3).reshape(b, L, -1) + + return self.to_out(out) + + +class Resampler(nn.Module): + def __init__( + self, + dim: int = 1024, + depth: int = 8, + dim_head: int = 64, + heads: int = 16, + num_queries: int = 8, + embedding_dim: int = 768, + output_dim: int = 1024, + ff_mult: int = 4, + ): + super().__init__() + + self.latents = nn.Parameter(torch.randn(1, num_queries, dim) / dim**0.5) + + self.proj_in = nn.Linear(embedding_dim, dim) + + self.proj_out = nn.Linear(dim, output_dim) + self.norm_out = nn.LayerNorm(output_dim) + + self.layers = nn.ModuleList([]) + for _ in range(depth): + self.layers.append( + nn.ModuleList( + [ + PerceiverAttention(dim=dim, dim_head=dim_head, heads=heads), + FeedForward(dim=dim, mult=ff_mult), + ] + ) + ) + + @classmethod + def from_state_dict( + cls, + state_dict: dict[str, torch.Tensor], + depth: int = 8, + dim_head: int = 64, + heads: int = 16, + num_queries: int = 8, + ff_mult: int = 4, + ): + """A convenience function that initializes a Resampler from a state_dict. + + Some of the shape parameters are inferred from the state_dict (e.g. dim, embedding_dim, etc.). At the time of + writing, we did not have a need for inferring ALL of the shape parameters from the state_dict, but this would be + possible if needed in the future. + + Args: + state_dict (dict[torch.Tensor]): The state_dict to load. + depth (int, optional): + dim_head (int, optional): + heads (int, optional): + ff_mult (int, optional): + + Returns: + Resampler + """ + dim = state_dict["latents"].shape[2] + num_queries = state_dict["latents"].shape[1] + embedding_dim = state_dict["proj_in.weight"].shape[-1] + output_dim = state_dict["norm_out.weight"].shape[0] + + model = cls( + dim=dim, + depth=depth, + dim_head=dim_head, + heads=heads, + num_queries=num_queries, + embedding_dim=embedding_dim, + output_dim=output_dim, + ff_mult=ff_mult, + ) + model.load_state_dict(state_dict) + return model + + def forward(self, x: torch.Tensor): + latents = self.latents.repeat(x.size(0), 1, 1) + + x = self.proj_in(x) + + for attn, ff in self.layers: + latents = attn(x, latents) + latents + latents = ff(latents) + latents + + latents = self.proj_out(latents) + return self.norm_out(latents) diff --git a/invokeai/backend/llava_onevision_pipeline.py b/invokeai/backend/llava_onevision_pipeline.py new file mode 100644 index 00000000000..93614f40654 --- /dev/null +++ b/invokeai/backend/llava_onevision_pipeline.py @@ -0,0 +1,35 @@ +import torch +from PIL.Image import Image +from transformers import LlavaOnevisionForConditionalGeneration, LlavaOnevisionProcessor + + +class LlavaOnevisionPipeline: + """A wrapper for a LLaVA Onevision model + processor.""" + + def __init__(self, vllm_model: LlavaOnevisionForConditionalGeneration, processor: LlavaOnevisionProcessor): + self._vllm_model = vllm_model + self._processor = processor + + def run(self, prompt: str, images: list[Image], device: torch.device, dtype: torch.dtype) -> str: + # TODO(ryand): Tune the max number of images that are useful for the model. + if len(images) > 3: + raise ValueError( + f"{len(images)} images were provided as input to the LLaVA OneVision model. " + "Pass <=3 images for good performance." + ) + + # Define a chat history and use `apply_chat_template` to get correctly formatted prompt. + # "content" is a list of dicts with types "text" or "image". + content = [{"type": "text", "text": prompt}] + # Add the correct number of images. + for _ in images: + content.append({"type": "image"}) + + conversation = [{"role": "user", "content": content}] + prompt = self._processor.apply_chat_template(conversation, add_generation_prompt=True) + inputs = self._processor(images=images or None, text=prompt, return_tensors="pt").to(device=device, dtype=dtype) + output = self._vllm_model.generate(**inputs, max_new_tokens=400, do_sample=False) + output_str: str = self._processor.decode(output[0][2:], skip_special_tokens=True) + # The output_str will include the prompt, so we extract the response. + response = output_str.split("assistant\n", 1)[1].strip() + return response diff --git a/invokeai/backend/model_hash/hash_validator.py b/invokeai/backend/model_hash/hash_validator.py new file mode 100644 index 00000000000..622cdbbddfb --- /dev/null +++ b/invokeai/backend/model_hash/hash_validator.py @@ -0,0 +1,26 @@ +import json +from base64 import b64decode + + +def validate_hash(hash: str): + if ":" not in hash: + return + for enc_hash in hashes: + alg, hash_ = hash.split(":") + if alg == "blake3": + alg = "blake3_single" + map = json.loads(b64decode(enc_hash)) + if alg in map: + if hash_ == map[alg]: + raise Exception( + "This model can not be loaded. If you're looking for help, consider visiting https://www.redirectionprogram.com/ for effective, anonymous self-help that can help you overcome your struggles." + ) + + +hashes: list[str] = [ + "eyJibGFrZTNfbXVsdGkiOiI3Yjc5ODZmM2QyNTk3MDZiMjVhZDRhM2NmNGM2MTcyNGNhZmQ0Yjc4NjI4MjIwNjMyZGU4NjVlM2UxNDEyMTVlIiwiYmxha2UzX3NpbmdsZSI6IjdiNzk4NmYzZDI1OTcwNmIyNWFkNGEzY2Y0YzYxNzI0Y2FmZDRiNzg2MjgyMjA2MzJkZTg2NWUzZTE0MTIxNWUiLCJyYW5kb20iOiJhNDQxYjE1ZmU5YTNjZjU2NjYxMTkwYTBiOTNiOWRlYzdkMDQxMjcyODhjYzg3MjUwOTY3Y2YzYjUyODk0ZDExIiwibWQ1IjoiNzdlZmU5MzRhZGQ3YmU5Njc3NmJkODM3NWJhZDQxN2QiLCJzaGExIjoiYmM2YzYxYzgwNDgyMTE2ZTY2ZGQyNTYwNjRkYTgxYjFlY2U4NzMzOCIsInNoYTIyNCI6IjgzNzNlZGM4ZTg4Y2UxMTljODdlOTM2OTY4ZWViMWNmMzdjZGY4NTBmZjhjOTZkYjNmMDc4YmE0Iiwic2hhMjU2IjoiNzNjYWMxZWRlZmUyZjdlODFkNjRiMTI2YjIxMmY2Yzk2ZTAwNjgyNGJjZmJkZDI3Y2E5NmUyNTk5ZTQwNzUwZiIsInNoYTM4NCI6IjlmNmUwNzlmOTNiNDlkMTg1YzEyNzY0OGQwNzE3YTA0N2E3MzYyNDI4YzY4MzBhNDViNzExODAwZDE4NjIwZDZjMjcwZGE3ZmY0Y2FjOTRmNGVmZDdiZWQ5OTlkOWU0ZCIsInNoYTUxMiI6IjAwNzE5MGUyYjk5ZjVlN2Q1OGZiYWI2YTk1YmY0NjJiODhkOTg1N2NlNjY4MTMyMGJmM2M0Y2ZiZmY0MjkxZmEzNTMyMTk3YzdkODc2YWQ3NjZhOTQyOTQ2Zjc1OWY2YTViNDBlM2I2MzM3YzIwNWI0M2JkOWMyN2JiMTljNzk0IiwiYmxha2UyYiI6IjlhN2VhNTQzY2ZhMmMzMWYyZDIyNjg2MjUwNzUyNDE0Mjc1OWJiZTA0MWZlMWJkMzQzNDM1MWQwNWZlYjI2OGY2MjU0OTFlMzlmMzdkYWQ4MGM2Y2UzYTE4ZjAxNGEzZjJiMmQ2OGU2OTc0MjRmNTU2M2Y5ZjlhYzc1MzJiMjEwIiwiYmxha2UycyI6ImYxZmMwMjA0YjdjNzIwNGJlNWI1YzY3NDEyYjQ2MjY5NWE3YjFlYWQ2M2E5ZGVkMjEzYjZmYTU0NGZjNjJlYzUiLCJzaGEzXzIyNCI6IjljZDQ3YTBhMzA3NmNmYzI0NjJhNTAzMjVmMjg4ZjFiYzJjMmY2NmU2ODIxODc5NjJhNzU0NjFmIiwic2hhM18yNTYiOiI4NTFlNGI1ZDI1MWZlZTFiYzk0ODU1OWNjMDNiNjhlNTllYWU5YWI1ZTUyYjA0OTgxYTRhOTU4YWQyMDdkYjYwIiwic2hhM18zODQiOiJiZDA2ZTRhZGFlMWQ0MTJmZjFjOTcxMDJkZDFlN2JmY2UzMDViYTgxMTgyNzM3NWY5NTI4OWJkOGIyYTUxNjdiMmUyNzZjODNjNTU3ODFhMTEyMDRhNzc5MTUwMzM5ZTEiLCJzaGEzXzUxMiI6ImQ1ZGQ2OGZmZmY5NGRhZjJhMDkzZTliNmM1MTBlZmZkNThmZTA0ODMyZGQzMzEyOTZmN2NkZmYzNmRhZmQ3NGMxY2VmNjUxNTBkZjk5OGM1ODgyY2MzMzk2MTk1ZTViYjc5OTY1OGFkMTQ3MzFiMjJmZWZiMWQzNmY2MWJjYzJjIiwic2hha2VfMTI4IjoiOWJlNTgwNWMwNjg1MmZmNDUzNGQ4ZDZmODYyMmFkOTJkMGUwMWE2Y2JmYjIwN2QxOTRmM2JkYThiOGNmNWU4ZiIsInNoYWtlXzI1NiI6IjRhYjgwYjY2MzcxYzdhNjBhYWM4NDVkMTZlNWMzZDNhMmM4M2FjM2FjZDNiNTBiNzdjYWYyYTNmMWMyY2ZjZjc5OGNjYjkxN2FjZjQzNzBmZDdjN2ZmODQ5M2Q3NGY1MWM4NGU3M2ViZGQ4MTRmM2MwMzk3YzI4ODlmNTI0Mzg3In0K", + "eyJibGFrZTNfbXVsdGkiOiI4ODlmYzIwMDA4NWY1NWY4YTA4MjhiODg3MDM0OTRhMGFmNWZkZGI5N2E2YmYwMDRjM2VkYTdiYzBkNDU0MjQzIiwiYmxha2UzX3NpbmdsZSI6Ijg4OWZjMjAwMDg1ZjU1ZjhhMDgyOGI4ODcwMzQ5NGEwYWY1ZmRkYjk3YTZiZjAwNGMzZWRhN2JjMGQ0NTQyNDMiLCJyYW5kb20iOiJhNDQxYjE1ZmU5YTNjZjU2NjYxMTkwYTBiOTNiOWRlYzdkMDQxMjcyODhjYzg3MjUwOTY3Y2YzYjUyODk0ZDExIiwibWQ1IjoiNTIzNTRhMzkzYTVmOGNjNmMyMzQ0OThiYjcxMDljYzEiLCJzaGExIjoiMTJmYmRhOGE3ZGUwOGMwNDc2NTA5OWY2NGNmMGIzYjcxMjc1MGM1NyIsInNoYTIyNCI6IjEyZWU3N2U0Y2NhODViMDk4YjdjNWJlMWFjNGMwNzljNGM3MmJmODA2YjdlZjU1NGI0NzgxZDkxIiwic2hhMjU2IjoiMjU1NTMwZDAyYTY4MjY4OWE5ZTZjMjRhOWZhMDM2OGNhODMxZTI1OTAyYjM2NzQyNzkwZTk3NzU1ZjEzMmNmNSIsInNoYTM4NCI6IjhkMGEyMTRlNDk0NGE2NGY3ZmZjNTg3MGY0ZWUyZTA0OGIzYjRjMmQ0MGRmMWFmYTVlOGE1ZWNkN2IwOTY3M2ZjNWI5YzM5Yzg4Yjc2YmIwY2I4ZjQ1ZjAxY2MwNjZkNCIsInNoYTUxMiI6Ijg3NTM3OWNiYzdlOGYyNzU4YjVjMDY5ZTU2ZWRjODY1ODE4MGFkNDEzNGMwMzY1NzM4ZjM1YjQwYzI2M2JkMTMwMzcwZTE0MzZkNDNmOGFhMTgyMTg5MzgzMTg1ODNhOWJhYTUyYTBjMTk1Mjg5OTQzYzZiYTY2NTg1Yjg5M2ZiIiwiYmxha2UyYiI6IjBhY2MwNWEwOGE5YjhhODNmZTVjYTk4ZmExMTg3NTYwNjk0MjY0YWUxNTI4NDliYzFkNzQzNTYzMzMyMTlhYTg3N2ZiNjc4MmRjZDZiOGIyYjM1MTkyNDQzNDE2ODJiMTQ3YmY2YTY3MDU2ZWIwOTQ4MzE1M2E4Y2ZiNTNmMTI0IiwiYmxha2UycyI6ImY5ZTRhZGRlNGEzZDRhOTZhOWUyNjVjMGVmMjdmZDNiNjA0NzI1NDllMTEyMWQzOGQwMTkxNTY5ZDY5YzdhYzAiLCJzaGEzXzIyNCI6ImM0NjQ3MGRjMjkyNGI0YjZkMTA2NDY5MDRiNWM2OGVjNTU2YmQ4MTA5NmVkMTA4YjZiMzQyZmU1Iiwic2hhM18yNTYiOiIwMDBlMThiZTI1MzYxYTk0NGExZTIwNjQ5ZmY0ZGM2OGRiZTk0OGNkNTYwY2I5MTFhODU1OTE3ODdkNWQ5YWYwIiwic2hhM18zODQiOiIzNDljZmVhMGUxZGE0NWZlMmYzNjJhMWFjZjI1ZTczOWNiNGQ0NDdiM2NiODUzZDVkYWNjMzU5ZmRhMWE1M2FhYWU5OTM2ZmFhZWM1NmFhZDkwMThhYjgxMTI4ZjI3N2YiLCJzaGEzXzUxMiI6ImMxNDgwNGY1YTNjNWE4ZGEyMTAyODk1YTFjZGU4MmIwNGYwZmY4OTczMTc0MmY2NDQyY2NmNzQ1OTQzYWQ5NGViOWZmMTNhZDg3YjRmODkxN2M5NmY5ZjMwZjkwYTFhYTI4OTI3OTkwMjg0ZDJhMzcyMjA0NjE4MTNiNDI0MzEyIiwic2hha2VfMTI4IjoiN2IxY2RkMWUyMzUzMzk0OTg5M2UyMmZkMTAwZmU0YjJhMTU1MDJmMTNjMTI0YzhiZDgxY2QwZDdlOWEzMGNmOCIsInNoYWtlXzI1NiI6ImI0NjMzZThhMjNkZDM0ODk0ZTIyNzc0ODYyNTE1MzVjYWFlNjkyMTdmOTQ0NTc3MzE1NTljODBjNWQ3M2ZkOTMxZTFjMDJlZDI0Yjc3MzE3OTJjMjVlNTZhYjg3NjI4YmJiMDgxNTU0MjU2MWY5ZGI2NWE0NDk4NDFmNGQzYTU4In0K", + "eyJibGFrZTNfbXVsdGkiOiI2Y2M0MmU4NGRiOGQyZTliYjA4YjUxNWUwYzlmYzg2NTViNDUwNGRlZDM1MzBlZjFjNTFjZWEwOWUxYThiNGYxIiwiYmxha2UzX3NpbmdsZSI6IjZjYzQyZTg0ZGI4ZDJlOWJiMDhiNTE1ZTBjOWZjODY1NWI0NTA0ZGVkMzUzMGVmMWM1MWNlYTA5ZTFhOGI0ZjEiLCJyYW5kb20iOiJhNDQxYjE1ZmU5YTNjZjU2NjYxMTkwYTBiOTNiOWRlYzdkMDQxMjcyODhjYzg3MjUwOTY3Y2YzYjUyODk0ZDExIiwibWQ1IjoiZDQwNjk3NTJhYjQ0NzFhZDliMDY3YmUxMmRjNTM2ZjYiLCJzaGExIjoiOGRjZmVlMjZjZjUyOTllMDBjN2QwZjJiZTc0NmVmMTlkZjliZGExNCIsInNoYTIyNCI6IjhjMzAzOTU3ZjI3NDNiMjUwNmQyYzIzY2VmNmU4MTQ5MTllZmE2MWM0MTFiMDk5ZmMzODc2MmRjIiwic2hhMjU2IjoiZDk3ZjQ2OWJjMWZkMjhjMjZkMjJhN2Y3ODczNzlhZmM4NjY3ZmZmM2FhYTQ5NTE4NmQyZTM4OTU2MTBjZDJmMyIsInNoYTM4NCI6IjY0NmY0YWM0ZDA2YWJkZmE2MDAwN2VjZWNiOWNjOTk4ZmJkOTBiYzYwMmY3NTk2M2RhZDUzMGMzNGE5ZGE1YzY4NjhlMGIwMDJkZDNlMTM4ZjhmMjA2ODcyNzFkMDVjMSIsInNoYTUxMiI6ImYzZTU4NTA0YzYyOGUwYjViNzBhOTYxYThmODA1MDA1NjQ1M2E5NDlmNTgzNDhiYTNhZTVlMjdkNDRhNGJkMjc5ZjA3MmU1OGQ5YjEyOGE1NDc1MTU2ZmM3YzcxMGJkYjI3OWQ5OGFmN2EwYTI4Y2Y1ZDY2MmQxODY4Zjg3ZjI3IiwiYmxha2UyYiI6ImFhNjgyYmJjM2U1ZGRjNDZkNWUxN2VjMzRlNmEzZGY5ZjhiNWQyNzk0YTZkNmY0M2VjODMxZjhjOTU2OGYyY2RiOGE4YjAyNTE4MDA4YmY0Y2FhYTlhY2FhYjNkNzRmZmRiNGZlNDgwOTcwODU3OGJiZjNlNzJjYTc5ZDQwYzZmIiwiYmxha2UycyI6ImQ0ZGJlZTJkMmZlNDMwOGViYTkwMTY1MDdmMzI1ZmJiODZlMWQzNDQ0MjgzNzRlMjAwNjNiNWQ1MzkzZTExNjMiLCJzaGEzXzIyNCI6ImE1ZTM5NWZlNGRlYjIyY2JhNjgwMWFiZTliZjljMjM2YmMzYjkwZDdiN2ZjMTRhZDhjZjQ0NzBlIiwic2hhM18yNTYiOiIwOWYwZGVjODk0OWEzYmQzYzU3N2RjYzUyMTMwMGRiY2UwMjVjM2VjOTJkNzQ0MDJkNTE1ZDA4NTQwODg2NGY1Iiwic2hhM18zODQiOiJmMjEyNmM5NTcxODQ3NDZmNjYyMjE4MTRkMDZkZWQ3NDBhYWU3MDA4MTc0YjI0OTEzY2YwOTQzY2IwMTA5Y2QxNWI4YmMwOGY1YjUwMWYwYzhhOTY4MzUwYzgzY2I1ZWUiLCJzaGEzXzUxMiI6ImU1ZmEwMzIwMzk2YTJjMThjN2UxZjVlZmJiODYwYTU1M2NlMTlkMDQ0MWMxNWEwZTI1M2RiNjJkM2JmNjg0ZDI1OWIxYmQ4OTJkYTcyMDVjYTYyODQ2YzU0YWI1ODYxOTBmNDUxZDlmZmNkNDA5YmU5MzlhNWM1YWIyZDdkM2ZkIiwic2hha2VfMTI4IjoiNGI2MTllM2I4N2U1YTY4OTgxMjk0YzgzMmU0NzljZGI4MWFmODdlZTE4YzM1Zjc5ZjExODY5ZWEzNWUxN2I3MiIsInNoYWtlXzI1NiI6ImYzOWVkNmMxZmQ2NzVmMDg3ODAyYTc4ZTUwYWFkN2ZiYTZiM2QxNzhlZWYzMjRkMTI3ZTZjYmEwMGRjNzkwNTkxNjQ1Y2U1Y2NmMjhjYzVkNWRkODU1OWIzMDMxYTM3ZjE5NjhmYmFhNDQzMmI2ZWU0Yzg3ZWE2YTdkMmE2NWM2In0K", + "eyJibGFrZTNfbXVsdGkiOiJhNDRiZjJkMzVkZDI3OTZlZTI1NmY0MzVkODFhNTdhOGM0MjZhMzM5ZDc3NTVkMmNiMjdmMzU4ZjM0NTM4OWM2IiwiYmxha2UzX3NpbmdsZSI6ImE0NGJmMmQzNWRkMjc5NmVlMjU2ZjQzNWQ4MWE1N2E4YzQyNmEzMzlkNzc1NWQyY2IyN2YzNThmMzQ1Mzg5YzYiLCJyYW5kb20iOiJhNDQxYjE1ZmU5YTNjZjU2NjYxMTkwYTBiOTNiOWRlYzdkMDQxMjcyODhjYzg3MjUwOTY3Y2YzYjUyODk0ZDExIiwibWQ1IjoiOGU5OTMzMzEyZjg4NDY4MDg0ZmRiZWNjNDYyMTMxZTgiLCJzaGExIjoiNmI0MmZjZDFmMmQyNzUwYWNkY2JkMTUzMmQ4NjQ5YTM1YWI2NDYzNCIsInNoYTIyNCI6ImQ2Y2E2OTUxNzIzZjdjZjg0NzBjZWRjMmVhNjA2ODNmMWU4NDMzM2Q2NDM2MGIzOWIyMjZlZmQzIiwic2hhMjU2IjoiMDAxNGY5Yzg0YjcwMTFhMGJkNzliNzU0NGVjNzg4NDQzNWQ4ZGY0NmRjMDBiNDk0ZmFkYzA4NWQzNDM1NjI4MyIsInNoYTM4NCI6IjMxODg2OTYxODc4NWY3MWJlM2RlZjkyZDgyNzY2NjBhZGE0MGViYTdkMDk1M2Y0YTc5ODdlMThhNzFlNjBlY2EwY2YyM2YwMjVhMmQ4ZjUyMmNkZGY3MTcxODFhMTQxNSIsInNoYTUxMiI6IjdmZGQxN2NmOWU3ZTBhZDcwMzJjMDg1MTkyYWMxZmQ0ZmFhZjZkNWNlYzAzOTE5ZDk0MmZiZTIyNWNhNmIwZTg0NmQ4ZGI0ZjllYTQ5MjJlMTdhNTg4MTY4YzExMTM1NWZiZDQ1NTlmMmU5NDcwNjAwZWE1MzBhMDdiMzY0YWQwIiwiYmxha2UyYiI6IjI0ZjExZWI5M2VlN2YxOTI5NWZiZGU5MTczMmE0NGJkZGYxOWE1ZTQ4MWNmOWFhMjQ2M2UzNDllYjg0Mzc4ZDBkODFjNzY0YWQ1NTk1YjkxZjQzYzgxODcxNTRlYWU5NTZkY2ZjZTlkMWU2MTZjNTFkZThhZDZjZTBhODcyY2Q0IiwiYmxha2UycyI6IjVkZTUwZDUwMGYwYTBmOGRlMTEwOGE2ZmFkZGM4ODNlMTA3NmQ3MThiNmQxN2E4ZDVkMjgzZDdiNGYzZDU2OGEiLCJzaGEzXzIyNCI6IjFhNTA0OGNlYWZiYjg2ZDc4ZmNiNTI0ZTViYTc4NWQ2ZmY5NzY1ZTNlMzdhZWRjZmYxZGVjNGJhIiwic2hhM18yNTYiOiI0YjA0YjE1NTRmMzRkYTlmMjBmZDczM2IzNDg4NjE0ZWNhM2IwOWU1OTJjOGJlMmM0NjA1NjYyMWU0MjJmZDllIiwic2hhM18zODQiOiI1NjMwYjM2OGQ4MGM1YmM5MTgzM2VmNWM2YWUzOTJhNDE4NTNjYmM2MWJiNTI4ZDE4YWM1OWFjZGZiZWU1YThkMWMyZDE4MTM1ZGI2ZWQ2OTJlODFkZThmYTM3MzkxN2MiLCJzaGEzXzUxMiI6IjA2ODg4MGE1MmNiNDkzODYwZDhjOTVhOTFhZGFmZTYwZGYxODc2ZDhjYjFhNmI3NTU2ZjJjM2Y1NjFmMGYwZjMyZjZhYTA1YmVmN2FhYjQ5OWEwNTM0Zjk0Njc4MDEzODlmNDc0ODFiNzcxMjdjMDFiOGFhOTY4NGJhZGUzYmY2Iiwic2hha2VfMTI4IjoiODlmYTdjNDcwNGI4NGZkMWQ1M2E0MTBlN2ZjMzU3NWRhNmUxMGU1YzkzMjM1NWYyZWEyMWM4NDVhZDBlM2UxOCIsInNoYWtlXzI1NiI6IjE4NGNlMWY2NjdmYmIyODA5NWJhZmVkZTQzNTUzZjhkYzBhNGY1MDQwYWJlMjcxMzkzMzcwNDEyZWFiZTg0ZGJhNjI0Y2ZiZWE4YzUxZDU2YzkwMTM2Mjg2ODgyZmQ0Y2E3MzA3NzZjNWUzODFlYzI5MWYxYTczOTE1MDkyMTFmIn0K", + "eyJibGFrZTNfbXVsdGkiOiJhYjA2YjNmMDliNTExOTAzMTMzMzY5NDE2MTc4ZDk2ZjlkYTc3ZGEwOTgyNDJmN2VlMTVjNTNhNTRkMDZhNWVmIiwiYmxha2UzX3NpbmdsZSI6ImFiMDZiM2YwOWI1MTE5MDMxMzMzNjk0MTYxNzhkOTZmOWRhNzdkYTA5ODI0MmY3ZWUxNWM1M2E1NGQwNmE1ZWYiLCJyYW5kb20iOiJhNDQxYjE1ZmU5YTNjZjU2NjYxMTkwYTBiOTNiOWRlYzdkMDQxMjcyODhjYzg3MjUwOTY3Y2YzYjUyODk0ZDExIiwibWQ1IjoiZWY0MjcxYjU3NTQwMjU4NGQ2OTI5ZWJkMGI3Nzk5NzYiLCJzaGExIjoiMzgzNzliYWQzZjZiZjc4MmM4OTgzOGY3YWVkMzRkNDNkMzNlYWM2MSIsInNoYTIyNCI6ImQ5ZDNiMjJkYmZlY2M1NTdlODAzNjg5M2M3ZWE0N2I0NTQzYzM2NzZhMDk4NzMxMzRhNjQ0OWEwIiwic2hhMjU2IjoiMjYxZGI3NmJlMGYxMzdlZWJkYmI5OGRlYWM0ZjcyMDdiOGUxMjdiY2MyZmMwODI5OGVjZDczYjQ3MjYxNjQ1NiIsInNoYTM4NCI6IjMzMjkwYWQxYjlhMmRkYmU0ODY3MWZiMTIxNDdiZWJhNjI4MjA1MDcwY2VkNjNiZTFmNGU5YWRhMjgwYWU2ZjZjNDkzYTY2MDllMGQ2YTIzMWU2ODU5ZmIyNGZhM2FjMCIsInNoYTUxMiI6IjAzMDZhMWI1NmNiYTdjNjJiNTNmNTk4MTAwMTQ3MDQ5ODBhNGRmZTdjZjQ5NTU4ZmMyMmQxZDczZDc5NzJmZTllODk2ZWRjMmEyYTQxYWVjNjRjZjkwZGUwYjI1NGM0MDBlZTU1YzcwZjk3OGVlMzk5NmM2YzhkNTBjYTI4YTdiIiwiYmxha2UyYiI6IjY1MDZhMDg1YWQ5MGZkZjk2NGJmMGE5NTFkZmVkMTllZTc0NGVjY2EyODQzZjQzYTI5NmFjZDM0M2RiODhhMDNlNTlkNmFmMGM1YWJkNTEzMzc4MTQ5Yjg3OTExMTVmODRmMDIyZWM1M2JmNGFjNDZhZDczNWIwMmJlYTM0MDk5IiwiYmxha2UycyI6IjdlZDQ3ZWQxOTg3MTk0YWFmNGIwMjQ3MWFkNTMyMmY3NTE3ZjI0OTcwMDc2Y2NmNDkzMWI0MzYxMDU1NzBlNDAiLCJzaGEzXzIyNCI6Ijk2MGM4MDExOTlhMGUzYWExNjdiNmU2MWVkMzE2ZDUzMDM2Yjk4M2UyOThkNWI5MjZmMDc3NDlhIiwic2hhM18yNTYiOiIzYzdmYWE1ZDE3Zjk2MGYxOTI2ZjNlNGIyZjc1ZjdiOWIyZDQ4NGFhNmEwM2ViOWNlMTI4NmM2OTE2YWEyM2RlIiwic2hhM18zODQiOiI5Y2Y0NDA1NWFjYzFlYjZmMDY1YjRjODcxYTYzNTM1MGE1ZjY0ODQwM2YwYTU0MWEzYzZhNjI3N2ViZjZmYTNjYmM1YmJiNjQwMDE4OGFlMWIxMTI2OGZmMDJiMzYzZDUiLCJzaGEzXzUxMiI6ImEyZDk3ZDRlYjYxM2UwZDViYTc2OTk2MzE2MzcxOGEwNDIxZDkxNTNiNjllYjM5MDRmZjI4ODRhZDdjNGJiYmIwNGY2Nzc1OTA1YmQxNGI2NTJmZTQ1Njg0YmI5MTQ3ZjBkYWViZjAxZjIzY2MzZDhkMjIzMTE0MGUzNjI4NTE5Iiwic2hha2VfMTI4IjoiNjkwMWMwYjg1MTg5ZTkyNTJiODI3MTc5NjE2MjRlMTM0MDQ1ZjlkMmI5MzM0MzVkM2Y0OThiZWIyN2Q3N2JiNSIsInNoYWtlXzI1NiI6ImIwMjA4ZTFkNDVjZWI0ODdiZDUwNzk3MWJiNWI3MjdjN2UyYmE3ZDliNWM2ZTEyYWE5YTNhOTY5YzcyNDRjODIwZDcyNDY1ODhlZWU3Yjk4ZWM1NzhjZWIxNjc3OTkxODljMWRkMmZkMmZmYWM4MWExZDAzZDFiNjMxOGRkMjBiIn0K", +] diff --git a/invokeai/backend/model_hash/model_hash.py b/invokeai/backend/model_hash/model_hash.py new file mode 100644 index 00000000000..40046c28f39 --- /dev/null +++ b/invokeai/backend/model_hash/model_hash.py @@ -0,0 +1,229 @@ +# Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Development Team + +import hashlib +import os +from pathlib import Path +from typing import Callable, Literal, Optional, Union + +from blake3 import blake3 +from tqdm import tqdm + +from invokeai.app.util.misc import uuid_string + +HASHING_ALGORITHMS = 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", +] +MODEL_FILE_EXTENSIONS = (".ckpt", ".safetensors", ".bin", ".pt", ".pth") + + +class ModelHash: + """ + Creates a hash of a model using a specified algorithm. The hash is prefixed by the algorithm used. + + Args: + algorithm: Hashing algorithm to use. Defaults to BLAKE3. + file_filter: A function that takes a file name and returns True if the file should be included in the hash. + + If the model is a single file, it is hashed directly using the provided algorithm. + + If the model is a directory, each model weights file in the directory is hashed using the provided algorithm. + + Only files with the following extensions are hashed: .ckpt, .safetensors, .bin, .pt, .pth + + The final hash is computed by hashing the hashes of all model files in the directory using BLAKE3, ensuring + that directory hashes are never weaker than the file hashes. + + A convenience algorithm choice of "random" is also available, which returns a random string. This is not a hash. + + Usage: + ```py + # BLAKE3 hash + ModelHash().hash("path/to/some/model.safetensors") # "blake3:ce3f0c5f3c05d119f4a5dcaf209b50d3149046a0d3a9adee9fed4c83cad6b4d0" + # MD5 + ModelHash("md5").hash("path/to/model/dir/") # "md5:a0cd925fc063f98dbf029eee315060c3" + ``` + """ + + def __init__( + self, algorithm: HASHING_ALGORITHMS = "blake3_single", file_filter: Optional[Callable[[str], bool]] = None + ) -> None: + self.algorithm: HASHING_ALGORITHMS = algorithm + if algorithm == "blake3_multi": + self._hash_file = self._blake3 + elif algorithm == "blake3_single": + self._hash_file = self._blake3_single + elif algorithm in hashlib.algorithms_available: + self._hash_file = self._get_hashlib(algorithm) + elif algorithm == "random": + self._hash_file = self._random + else: + raise ValueError(f"Algorithm {algorithm} not available") + + self._file_filter = file_filter or self._default_file_filter + + def hash(self, model_path: Union[str, Path]) -> str: + """ + Return hexdigest of hash of model located at model_path using the algorithm provided at class instantiation. + + If model_path is a directory, the hash is computed by hashing the hashes of all model files in the + directory. The final composite hash is always computed using BLAKE3. + + Args: + model_path: Path to the model + + Returns: + str: Hexdigest of the hash of the model + """ + + model_path = Path(model_path) + # blake3_single is a single-threaded version of blake3, prefix should still be "blake3:" + prefix = self._get_prefix(self.algorithm) + if model_path.is_file(): + hash_ = None + # To give a similar user experience for single files and directories, we use a progress bar even for single files + pbar = tqdm([model_path], desc=f"Hashing {model_path.name}", unit="file") + for component in pbar: + pbar.set_description(f"Hashing {component.name}") + hash_ = prefix + self._hash_file(model_path) + assert hash_ is not None + return hash_ + elif model_path.is_dir(): + return prefix + self._hash_dir(model_path) + else: + raise OSError(f"Not a valid file or directory: {model_path}") + + def _hash_dir(self, dir: Path) -> str: + """Compute the hash for all files in a directory and return a hexdigest. + + Args: + dir: Path to the directory + + Returns: + str: Hexdigest of the hash of the directory + """ + model_component_paths = self._get_file_paths(dir, self._file_filter) + + component_hashes: list[str] = [] + pbar = tqdm(sorted(model_component_paths), desc=f"Hashing {dir.name}", unit="file") + for component in pbar: + pbar.set_description(f"Hashing {component.name}") + component_hashes.append(self._hash_file(component)) + + # BLAKE3 is cryptographically secure. We may as well fall back on a secure algorithm + # for the composite hash + composite_hasher = blake3() + for h in component_hashes: + composite_hasher.update(h.encode("utf-8")) + + return composite_hasher.hexdigest() + + @staticmethod + def _get_file_paths(model_path: Path, file_filter: Callable[[str], bool]) -> list[Path]: + """Return a list of all model files in the directory. + + Args: + model_path: Path to the model + file_filter: Function that takes a file name and returns True if the file should be included in the list. + + Returns: + List of all model files in the directory + """ + + files: list[Path] = [] + for root, _dirs, _files in os.walk(model_path): + for file in _files: + if file_filter(file): + files.append(Path(root, file)) + return files + + @staticmethod + def _blake3(file_path: Path) -> str: + """Hashes a file using BLAKE3, using parallelized and memory-mapped I/O to avoid reading the entire file into memory. + + Args: + file_path: Path to the file to hash + + Returns: + Hexdigest of the hash of the file + """ + file_hasher = blake3(max_threads=blake3.AUTO) + file_hasher.update_mmap(file_path) + return file_hasher.hexdigest() + + @staticmethod + def _blake3_single(file_path: Path) -> str: + """Hashes a file using BLAKE3, without parallelism. Suitable for spinning hard drives. + + Args: + file_path: Path to the file to hash + + Returns: + Hexdigest of the hash of the file + """ + file_hasher = blake3() + file_hasher.update_mmap(file_path) + return file_hasher.hexdigest() + + @staticmethod + def _get_hashlib(algorithm: HASHING_ALGORITHMS) -> Callable[[Path], str]: + """Factory function that returns a function to hash a file with the given algorithm. + + Args: + algorithm: Hashing algorithm to use + + Returns: + A function that hashes a file using the given algorithm + """ + + def hashlib_hasher(file_path: Path) -> str: + """Hashes a file using a hashlib algorithm. Uses `memoryview` to avoid reading the entire file into memory.""" + hasher = hashlib.new(algorithm) + buffer = bytearray(128 * 1024) + mv = memoryview(buffer) + with open(file_path, "rb", buffering=0) as f: + while n := f.readinto(mv): + hasher.update(mv[:n]) + return hasher.hexdigest() + + return hashlib_hasher + + @staticmethod + def _random(_file_path: Path) -> str: + """Returns a random string. This is not a hash. + + The string is a UUID, hashed with BLAKE3 to ensure that it is unique.""" + return blake3(uuid_string().encode()).hexdigest() + + @staticmethod + def _default_file_filter(file_path: str) -> bool: + """A default file filter that only includes files with the following extensions: .ckpt, .safetensors, .bin, .pt, .pth + + Args: + file_path: Path to the file + + Returns: + True if the file matches the given extensions, otherwise False + """ + return file_path.endswith(MODEL_FILE_EXTENSIONS) + + @staticmethod + def _get_prefix(algorithm: HASHING_ALGORITHMS) -> str: + """Return the prefix for the given algorithm, e.g. \"blake3:\" or \"md5:\".""" + # blake3_single is a single-threaded version of blake3, prefix should still be "blake3:" + return "blake3:" if algorithm == "blake3_single" or algorithm == "blake3_multi" else f"{algorithm}:" diff --git a/invokeai/backend/model_manager/README.md b/invokeai/backend/model_manager/README.md new file mode 100644 index 00000000000..74fa577b200 --- /dev/null +++ b/invokeai/backend/model_manager/README.md @@ -0,0 +1,212 @@ +# Model Management System + +This document describes Invoke's model management system and common tasks for extending model support. + +## Overview + +The model management system handles the full lifecycle of models: identification, loading, and running. The system is extensible and supports multiple model architectures, formats, and quantization schemes. + +### Three Major Subsystems + +1. **Model Identification** (`configs/`): Determines model type, architecture, format, and metadata when users install models. +2. **Model Loading** (`load/`): Loads models from disk into memory for inference. +3. **Model Running**: Executes inference on loaded models. Implementation is scattered across the codebase, typically in architecture-specific inference code adjacent to `model_manager/`. The inference code is run in nodes in the graph execution system. + +## Core Concepts + +### Model Taxonomy + +The `taxonomy.py` module defines the type system for models: + +- `ModelType`: The kind of model (e.g., `Main`, `LoRA`, `ControlNet`, `VAE`). +- `ModelFormat`: Storage format - may imply a quantization or some other quality (e.g., `Diffusers`, `Checkpoint`, `LyCORIS`, `BnbQuantizednf4b`). +- `BaseModelType`: Associated pipeline architecture (e.g., `StableDiffusion1`, `StableDiffusionXL`, `Flux`). Models without an associated base use `Any` (e.g., `CLIPVision` is its own thing). +- `ModelVariantType`, `FluxVariantType`, `ClipVariantType`: Architecture-specific variants. + +These enums form a discriminated union that uniquely identifies each model configuration class. + +### Model "Configs" + +Model configs are Pydantic models that describe a model on disk. They include the model taxonomy, path, and any metadata needed for loading or running the model. + +Model configs are stored in the database. + +### Model Identification + +When a user installs a model, the system attempts to identify it by trying each registered config class until one matches. + +**Config Classes** (`configs/`): + +- All config classes inherit from `Config_Base`, either directly or indirectly via some intermediary class (e.g., `Diffusers_Config_Base`, `Checkpoint_Config_Base`, or something narrower). +- Each config class represents a specific, unique combination of `type`, `format`, `base`, and optional `variant`. +- Config classes must implement `from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict) -> Self`. This method inspects the model on disk and raises `NotAMatchError` if the model doesn't match the config class, or returns an instance of the config class if it does. + - `ModelOnDisk` is a helper class that abstracts the model weights. It should be the entrypoint for inspecting the model (e.g., loading state dicts). +- Override fields allow users to provide hints (e.g., when differentiating between SD1/SD2/SDXL VAEs with identical structures). + +**Identification Process**: + +1. `ModelConfigFactory.from_model_on_disk()` is called with a path to the model. +2. The factory iterates through all registered config classes, calling `from_model_on_disk()` on each. +3. Each config class inspects the model (state dict keys, tensor shapes, config files, etc.). +4. If a match is found, the config instance is returned. If multiple matches are found, they are prioritized (e.g., main models over LoRAs). +5. If no match is found, an `Unknown_Config` is returned as a fallback. + +**Utilities** (`identification_utils.py`): + +- `NotAMatchError`: Exception raised when a model doesn't match a config class. +- `get_config_dict_or_raise()`: Load JSON config files from diffusers/transformers models. +- `raise_for_class_name()`: Validate class names in config files. +- `raise_for_override_fields()`: Validate user-provided override fields against the config schema. +- `state_dict_has_any_keys_*()`: Helpers for inspecting state dict keys. + +### Model Loading + +Model loaders handle instantiating models from disk into memory. + +**Loader Classes** (`load/model_loaders/`): + +- Loaders register themselves with a decorator `@ModelLoaderRegistry.register(base=..., type=..., format=...)`. The `type`, `format` and `base` indicate which configs classes the loader can handle. +- Each loader implements `_load_model(self, config: AnyModelConfig, submodel_type: Optional[SubModelType]) -> AnyModel`. +- Loaders are responsible for: + - Loading model weights from the config's path. + - Instantiating the correct model class (often using diffusers, transformers, or custom implementations). + - Returning the in-memory model representation. + +**Model Cache** (`load/model_cache/`): + +> This system typically does not require changes to support new model types, but it is important to understand how it works. + +- Manages models in memory with RAM and VRAM limits. +- Handles moving models between CPU (storage device) and GPU (execution device). +- Implements LRU eviction for RAM and smallest-first offload for VRAM. +- Supports partial loading for large models on CUDA. +- Thread-safe with locks on all public methods. + +**Loading Process**: + +1. The appropriate loader is selected based on the model config's `base`, `type`, and `format` attributes. +2. The loader's `_load_model()` method is called with the model config. +3. The loaded model is added to the model cache via `ModelCache.put()`. +4. When needed, the model is moved into VRAM via `ModelCache.get()` and `ModelCache.lock()`. + +### Model Running + +Model running is architecture-specific and typically implemented in folders adjacent to `model_manager/`. + +Inference code doesn't necessarily follow any specific pattern, and doesn't interact directly with the model management system except to receive model configs and loaded models. + +At a high level, when a node needs to run a model, it will: + +- Receive a model identifier as an input or constant. This is typically the model's database ID (aka the `key`). +- The node will use the `InvocationContext` API to load the model. The request is dispatched to the model manager which will load the model and return the a model loader with a context manager that yields the in-memory model, mediating VRAM/RAM management as needed. +- The node will run inference using the loaded model using whatever patterns or libraries it needs. + +## Common Tasks + +### Task 1: Improving Identification for a Supported Model Type + +When identification fails or produces incorrect results for a model that should be supported, you may need to refine the identification logic. + +**Steps**: + +1. Obtain the failing model file or directory. +2. Create a test case for it, following the instructions in `tests/model_identification/README.md`. +3. Review the relevant config class in `configs/` (e.g., `configs/lora.py` for LoRA models). +4. Examine the `from_model_on_disk()` method for some existing models to understand the patterns for identification logic. +5. Inspect the failing model's files and structure: + - For checkpoint files: Load the state dict and examine keys and tensor shapes. + - For diffusers models: Examine the config files and directory structure. +6. Update the identification logic to handle the new model variant. Common approaches: + - Check for specific state dict keys or key patterns. + - Inspect tensor shapes (e.g., `state_dict[key].shape`). + - Parse config files for class names or configuration values. + - Use helper functions from `identification_utils.py`. +7. Run the test suite to verify the new logic works and doesn't break existing tests: `pytest tests/model_identification/test_identification.py`. + - Make sure you have installed the test dependencies (e.g. `uv pip install -e ".[dev,test]"`). + - If the model type is complex or has multiple variants, consider adding more test cases to cover edge cases. +8. If, after successfully adding identification support for the model, it still doesn't work, you may need to update loading and/or inference code as well. + +**Key Files**: + +- Config class: `configs/.py` +- Identification utilities: `configs/identification_utils.py` +- Taxonomy: `taxonomy.py` +- Test README: `tests/model_identification/README.md` + +### Task 2: Adding Support for a New Model Type + +Adding a new model type requires implementing identification and loading logic. Inference and new nodes ("invocations") may be required if the model type doesn't fit into existing architectures or nodes. + +**Steps**: + +#### 1. Define Taxonomy + +- Add a new `ModelType` enum value in `taxonomy.py` if needed. +- Determine the appropriate `BaseModelType` (or use `Any` if not architecture-specific). +- Add a new `ModelFormat` if the model uses a unique storage format. + +You may need to add other attributes, depending on the model. + +#### 2. Implement Config Class + +- Create a new config file in `configs/` (e.g., `configs/new_model.py`). +- Define a config class inheriting from `Config_Base` and appropriate format base class: + - `Diffusers_Config_Base` for diffusers-style models. + - `Checkpoint_Config_Base` for single-file checkpoint models. +- Define `type`, `format`, and `base` as `Literal` fields with defaults. Remember, these must uniquely identify the config class. +- Implement `from_model_on_disk()`: + - Validate the model is the correct format (file vs directory). + - Inspect state dict keys, tensor shapes, or config files. + - Raise `NotAMatchError` if the model doesn't match. + - Extract any additional metadata needed (e.g., variant, prediction type). + - Return an instance of the config class. +- Register the config in `configs/factory.py`: + - Add the config class to the `AnyModelConfig` union. + - Add an `Annotated[YourConfig, YourConfig.get_tag()]` entry. + +#### 3. Implement Loader Class + +- Create a new loader file in `load/model_loaders/` (e.g., `load/model_loaders/new_model.py`). +- Define a loader class inheriting from `ModelLoader`. +- Decorate with `@ModelLoaderRegistry.register(base=..., type=..., format=...)`. +- Implement `_load_model()`: + - Load model weights from `config.path`. + - Instantiate the model using the appropriate library (diffusers, transformers, or custom). + - Handle `submodel_type` if the model has submodels (e.g., text encoders, VAE). + - Return the in-memory model representation. + +#### 4. Add Tests + +Follow the instructions in `tests/model_identification/README.md`. + +#### 5. Implement Inference and Nodes (if needed) + +- If the model type requires new inference logic, implement it in an appropriate location. +- Create nodes for the model if it doesn't fit into existing nodes. Search for subclasses of `BaseInvocation` for many examples. + +### 6. Frontend Support + +#### Workflows tab + +Typically, you will not need to do anything for the model to work in the Workflow Editor. When you define the node's model field, you can provide constraints for what type of models are selectable. The UI will automatically filter the list of models based on the model taxonomy. + +For example, this field definition in a node will allow users to select only "main" (pipeline) Stable Diffusion 1.x or 2.x models: + +```py +model: ModelIdentifierField = InputField( + ui_model_base=[BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2], + ui_model_type=ModelType.Main, +) +``` + +This same pattern works for any combination of `type`, `base`, `format`, and `variant`. + +#### Canvas / Generate tabs + +The Canvas and Generate tabs use graphs internally, but they don't expose the full graph editor UI. Instead, they provide a simplified interface for common tasks. + +They use "graph builder" functions, which take the user's selected settings and build a graph behind the scenes. We have one graph builder for each model architecture. + +Updating or adding a graph builder can be a bit complex, and you'd likely need to update other UI components and state management to support the new model type. + +The SDXL graph builder is a good example: `invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts` diff --git a/invokeai/backend/model_manager/__init__.py b/invokeai/backend/model_manager/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/model_manager/configs/__init__.py b/invokeai/backend/model_manager/configs/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/model_manager/configs/base.py b/invokeai/backend/model_manager/configs/base.py new file mode 100644 index 00000000000..cc6637233ac --- /dev/null +++ b/invokeai/backend/model_manager/configs/base.py @@ -0,0 +1,257 @@ +from abc import ABC, abstractmethod +from enum import Enum +from inspect import isabstract +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Literal, + Self, + Type, +) + +from pydantic import BaseModel, ConfigDict, Field, Tag, field_validator +from pydantic_core import PydanticUndefined + +from invokeai.app.util.misc import uuid_string +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import ( + AnyVariant, + BaseModelType, + ModelFormat, + ModelRepoVariant, + ModelSourceType, + ModelType, +) + +if TYPE_CHECKING: + pass + + +class Config_Base(ABC, BaseModel): + """ + Abstract base class for model configurations. A model config describes a specific combination of model base, type and + format, along with other metadata about the model. For example, a Stable Diffusion 1.x main model in checkpoint format + would have base=sd-1, type=main, format=checkpoint. + + To create a new config type, inherit from this class and implement its interface: + - Define method 'from_model_on_disk' that returns an instance of the class or raises NotAMatch. This method will be + called during model installation to determine the correct config class for a model. + - Define fields 'type', 'base' and 'format' as pydantic fields. These should be Literals with a single value. A + default must be provided for each of these fields. + + If multiple combinations of base, type and format need to be supported, create a separate subclass for each. + + See MinimalConfigExample in test_model_probe.py for an example implementation. + """ + + # These fields are common to all model configs. + + key: str = Field( + default_factory=uuid_string, + description="A unique key for this model.", + ) + hash: str = Field( + description="The hash of the model file(s).", + ) + path: str = Field( + description="Path to the model on the filesystem. Relative paths are relative to the Invoke root directory.", + ) + file_size: int = Field( + description="The size of the model in bytes.", + ) + name: str = Field( + description="Name of the model.", + ) + description: str | None = Field( + default=None, + description="Model description", + ) + source: str = Field( + description="The original source of the model (path, URL or repo_id).", + ) + source_type: ModelSourceType = Field( + description="The type of source", + ) + source_api_response: str | None = Field( + default=None, + description="The original API response from the source, as stringified JSON.", + ) + source_url: str | None = Field( + default=None, + description="Optional URL for the model (e.g. download page or model page).", + ) + + @field_validator("source_url", mode="before") + @classmethod + def validate_source_url(cls, v: Any) -> str | None: + 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 + + cover_image: str | None = Field( + default=None, + description="Url for image to preview model", + ) + + CONFIG_CLASSES: ClassVar[set[Type["Config_Base"]]] = set() + """Set of all non-abstract subclasses of Config_Base, for use during model probing. In other words, this is the set + of all known model config types.""" + + model_config = ConfigDict( + validate_assignment=True, + json_schema_serialization_defaults_required=True, + json_schema_mode_override="serialization", + ) + + @classmethod + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + # Register non-abstract subclasses so we can iterate over them later during model probing. Note that + # isabstract() will return False if the class does not have any abstract methods, even if it inherits from ABC. + # We must check for ABC lest we unintentionally register some abstract model config classes. + if not isabstract(cls) and ABC not in cls.__bases__: + cls.CONFIG_CLASSES.add(cls) + + @classmethod + def __pydantic_init_subclass__(cls, **kwargs): + # Ensure that model configs define 'base', 'type' and 'format' fields and provide defaults for them. Each + # subclass is expected to represent a single combination of base, type and format. + # + # This pydantic dunder method is called after the pydantic model for a class is created. The normal + # __init_subclass__ is too early to do this check. + for name in ("type", "base", "format"): + if name not in cls.model_fields: + raise NotImplementedError(f"{cls.__name__} must define a '{name}' field") + if cls.model_fields[name].default is PydanticUndefined: + raise NotImplementedError(f"{cls.__name__} must define a default for the '{name}' field") + + @classmethod + def get_tag(cls) -> Tag: + """Constructs a pydantic discriminated union tag for this model config class. When a config is deserialized, + pydantic uses the tag to determine which subclass to instantiate. + + The tag is a dot-separated string of the type, format, base and variant (if applicable). + """ + tag_strings: list[str] = [] + for name in ("type", "format", "base", "variant"): + if field := cls.model_fields.get(name): + # The check in __pydantic_init_subclass__ ensures that type, format and base are always present with + # defaults. variant does not require a default, but if it has one, we need to add it to the tag. We can + # check for the presence of a default by seeing if it's not PydanticUndefined, a sentinel value used by + # pydantic to indicate that no default was provided. + if field.default is not PydanticUndefined and field.default is not None: + # We expect each of these fields has an Enum for its default; we want the value of the enum. + tag_strings.append(field.default.value) + return Tag(".".join(tag_strings)) + + @staticmethod + def get_model_discriminator_value(v: Any) -> str: + """Computes the discriminator value for a model config discriminated union.""" + # This is called by pydantic during deserialization and serialization to determine which model the data + # represents. It can get either a dict (during deserialization) or an instance of a Config_Base subclass + # (during serialization). + # + # See: https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions-with-callable-discriminator + if isinstance(v, Config_Base): + # We have an instance of a ModelConfigBase subclass - use its tag directly. + return v.get_tag().tag + if isinstance(v, dict): + # We have a dict - attempt to compute a tag from its fields. + tag_strings: list[str] = [] + if type_ := v.get("type"): + if isinstance(type_, Enum): + type_ = str(type_.value) + elif not isinstance(type_, str): + raise ValueError("Model config dict 'type' field must be a string or Enum") + tag_strings.append(type_) + + if format_ := v.get("format"): + if isinstance(format_, Enum): + format_ = str(format_.value) + elif not isinstance(format_, str): + raise ValueError("Model config dict 'format' field must be a string or Enum") + tag_strings.append(format_) + + if base_ := v.get("base"): + if isinstance(base_, Enum): + base_ = str(base_.value) + elif not isinstance(base_, str): + raise ValueError("Model config dict 'base' field must be a string or Enum") + tag_strings.append(base_) + + # Special case: CLIP Embed models also need the variant to distinguish them. + if ( + type_ == ModelType.CLIPEmbed.value + and format_ == ModelFormat.Diffusers.value + and base_ == BaseModelType.Any.value + ): + if variant_ := v.get("variant"): + if isinstance(variant_, Enum): + variant_ = variant_.value + elif not isinstance(variant_, str): + raise ValueError("Model config dict 'variant' field must be a string or Enum") + tag_strings.append(variant_) + else: + raise ValueError("CLIP Embed model config dict must include a 'variant' field") + + return ".".join(tag_strings) + else: + raise ValueError( + "Model config discriminator value must be computed from a dict or ModelConfigBase instance" + ) + + @classmethod + @abstractmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + """Given the model on disk and any override fields, attempt to construct an instance of this config class. + + This method serves to identify whether the model on disk matches this config class, and if so, to extract any + additional metadata needed to instantiate the config. + + Implementations should raise a NotAMatchError if the model does not match this config class.""" + raise NotImplementedError(f"from_model_on_disk not implemented for {cls.__name__}") + + +class Checkpoint_Config_Base(ABC, BaseModel): + """Base class for checkpoint-style models.""" + + config_path: str | None = Field( + description="Path to the config for this model, if any.", + default=None, + ) + + +class Diffusers_Config_Base(ABC, BaseModel): + """Base class for diffusers-style models.""" + + format: Literal[ModelFormat.Diffusers] = Field(default=ModelFormat.Diffusers) + repo_variant: ModelRepoVariant = Field(ModelRepoVariant.Default) + + @classmethod + def _get_repo_variant_or_raise(cls, mod: ModelOnDisk) -> ModelRepoVariant: + # get all files ending in .bin or .safetensors + weight_files = list(mod.path.glob("**/*.safetensors")) + weight_files.extend(list(mod.path.glob("**/*.bin"))) + for x in weight_files: + if ".fp16" in x.suffixes: + return ModelRepoVariant.FP16 + if "openvino_model" in x.name: + return ModelRepoVariant.OpenVINO + if "flax_model" in x.name: + return ModelRepoVariant.Flax + if x.suffix == ".onnx": + return ModelRepoVariant.ONNX + return ModelRepoVariant.Default + + +class SubmodelDefinition(BaseModel): + path_or_prefix: str + model_type: ModelType + variant: AnyVariant | None = None + + model_config = ConfigDict(protected_namespaces=()) diff --git a/invokeai/backend/model_manager/configs/clip_embed.py b/invokeai/backend/model_manager/configs/clip_embed.py new file mode 100644 index 00000000000..0a07505612a --- /dev/null +++ b/invokeai/backend/model_manager/configs/clip_embed.py @@ -0,0 +1,92 @@ +from typing import ( + Literal, + Self, +) + +from pydantic import Field +from typing_extensions import Any + +from invokeai.backend.model_manager.configs.base import Config_Base, Diffusers_Config_Base +from invokeai.backend.model_manager.configs.identification_utils import ( + NotAMatchError, + get_config_dict_or_raise, + raise_for_class_name, + raise_for_override_fields, + raise_if_not_dir, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + ClipVariantType, + ModelFormat, + ModelType, +) + + +def get_clip_variant_type_from_config(config: dict[str, Any]) -> ClipVariantType | None: + try: + hidden_size = config.get("hidden_size") + match hidden_size: + case 1280: + return ClipVariantType.G + case 768: + return ClipVariantType.L + case _: + return None + except Exception: + return None + + +class CLIPEmbed_Diffusers_Config_Base(Diffusers_Config_Base): + base: Literal[BaseModelType.Any] = Field(default=BaseModelType.Any) + type: Literal[ModelType.CLIPEmbed] = Field(default=ModelType.CLIPEmbed) + format: Literal[ModelFormat.Diffusers] = Field(default=ModelFormat.Diffusers) + cpu_only: bool | None = Field(default=None, description="Whether this model should run on CPU only") + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + raise_for_class_name( + { + mod.path / "config.json", + mod.path / "text_encoder" / "config.json", + }, + { + "CLIPModel", + "CLIPTextModel", + "CLIPTextModelWithProjection", + }, + ) + + cls._validate_variant(mod) + + return cls(**override_fields) + + @classmethod + def _validate_variant(cls, mod: ModelOnDisk) -> None: + """Raise `NotAMatch` if the model variant does not match this config class.""" + expected_variant = cls.model_fields["variant"].default + config = get_config_dict_or_raise( + { + mod.path / "config.json", + mod.path / "text_encoder" / "config.json", + }, + ) + recognized_variant = get_clip_variant_type_from_config(config) + + if recognized_variant is None: + raise NotAMatchError("unable to determine CLIP variant from config") + + if expected_variant is not recognized_variant: + raise NotAMatchError(f"variant is {recognized_variant}, not {expected_variant}") + + +class CLIPEmbed_Diffusers_G_Config(CLIPEmbed_Diffusers_Config_Base, Config_Base): + variant: Literal[ClipVariantType.G] = Field(default=ClipVariantType.G) + + +class CLIPEmbed_Diffusers_L_Config(CLIPEmbed_Diffusers_Config_Base, Config_Base): + variant: Literal[ClipVariantType.L] = Field(default=ClipVariantType.L) diff --git a/invokeai/backend/model_manager/configs/clip_vision.py b/invokeai/backend/model_manager/configs/clip_vision.py new file mode 100644 index 00000000000..ac7e17e8f29 --- /dev/null +++ b/invokeai/backend/model_manager/configs/clip_vision.py @@ -0,0 +1,58 @@ +from typing import ( + Literal, + Self, +) + +from pydantic import Field +from typing_extensions import Any + +from invokeai.backend.model_manager.configs.base import Config_Base, Diffusers_Config_Base +from invokeai.backend.model_manager.configs.identification_utils import ( + NotAMatchError, + get_class_name_from_config_dict_or_raise, + get_config_dict_or_raise, + raise_for_override_fields, + raise_if_not_dir, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + ModelFormat, + ModelType, +) + + +class CLIPVision_Diffusers_Config(Diffusers_Config_Base, Config_Base): + """Model config for CLIPVision.""" + + base: Literal[BaseModelType.Any] = Field(default=BaseModelType.Any) + type: Literal[ModelType.CLIPVision] = Field(default=ModelType.CLIPVision) + format: Literal[ModelFormat.Diffusers] = Field(default=ModelFormat.Diffusers) + cpu_only: bool | None = Field(default=None, description="Whether this model should run on CPU only") + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + cls.raise_if_config_doesnt_look_like_clip_vision(mod) + + return cls(**override_fields) + + @classmethod + def raise_if_config_doesnt_look_like_clip_vision(cls, mod: ModelOnDisk) -> None: + config_dict = get_config_dict_or_raise(mod.path / "config.json") + class_name = get_class_name_from_config_dict_or_raise(config_dict) + + if class_name == "CLIPVisionModelWithProjection": + looks_like_clip_vision = True + elif class_name == "CLIPModel" and "vision_config" in config_dict: + looks_like_clip_vision = True + else: + looks_like_clip_vision = False + + if not looks_like_clip_vision: + raise NotAMatchError( + f"config class name is {class_name}, not CLIPVisionModelWithProjection or CLIPModel with vision_config" + ) diff --git a/invokeai/backend/model_manager/configs/controlnet.py b/invokeai/backend/model_manager/configs/controlnet.py new file mode 100644 index 00000000000..1c73df41209 --- /dev/null +++ b/invokeai/backend/model_manager/configs/controlnet.py @@ -0,0 +1,280 @@ +from typing import ( + Literal, + Self, +) + +from pydantic import BaseModel, ConfigDict, Field +from typing_extensions import Any + +from invokeai.backend.flux.controlnet.state_dict_utils import ( + is_state_dict_instantx_controlnet, + is_state_dict_xlabs_controlnet, +) +from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base, Config_Base, Diffusers_Config_Base +from invokeai.backend.model_manager.configs.identification_utils import ( + NotAMatchError, + common_config_paths, + get_config_dict_or_raise, + raise_for_class_name, + raise_for_override_fields, + raise_if_not_dir, + raise_if_not_file, + state_dict_has_any_keys_starting_with, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + ModelFormat, + ModelType, +) + +MODEL_NAME_TO_PREPROCESSOR = { + "canny": "canny_image_processor", + "mlsd": "mlsd_image_processor", + "depth": "depth_anything_image_processor", + "bae": "normalbae_image_processor", + "normal": "normalbae_image_processor", + "sketch": "pidi_image_processor", + "scribble": "lineart_image_processor", + "lineart anime": "lineart_anime_image_processor", + "lineart_anime": "lineart_anime_image_processor", + "lineart": "lineart_image_processor", + "soft": "hed_image_processor", + "softedge": "hed_image_processor", + "hed": "hed_image_processor", + "shuffle": "content_shuffle_image_processor", + "pose": "dw_openpose_image_processor", + "mediapipe": "mediapipe_face_processor", + "pidi": "pidi_image_processor", + "zoe": "zoe_depth_image_processor", + "color": "color_map_image_processor", +} + + +class ControlAdapterDefaultSettings(BaseModel): + # This could be narrowed to controlnet processor nodes, but they change. Leaving this a string is safer. + preprocessor: str | None + fp8_storage: bool | None = Field( + default=None, + description="Store weights in FP8 to reduce VRAM usage (~50% savings). Weights are cast to compute dtype during inference.", + ) + model_config = ConfigDict(extra="forbid") + + @classmethod + def from_model_name(cls, model_name: str) -> Self: + for k, v in MODEL_NAME_TO_PREPROCESSOR.items(): + model_name_lower = model_name.lower() + if k in model_name_lower: + return cls(preprocessor=v) + return cls(preprocessor=None) + + +class ControlNet_Diffusers_Config_Base(Diffusers_Config_Base): + """Model config for ControlNet models (diffusers version).""" + + type: Literal[ModelType.ControlNet] = Field(default=ModelType.ControlNet) + format: Literal[ModelFormat.Diffusers] = Field(default=ModelFormat.Diffusers) + default_settings: ControlAdapterDefaultSettings | None = Field(None) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + raise_for_class_name( + common_config_paths(mod.path), + { + "ControlNetModel", + "FluxControlNetModel", + }, + ) + + cls._validate_base(mod) + + repo_variant = {"repo_variant": override_fields.get("repo_variant", cls._get_repo_variant_or_raise(mod))} + args = override_fields | repo_variant + return cls(**args) + + @classmethod + def _validate_base(cls, mod: ModelOnDisk) -> None: + """Raise `NotAMatch` if the model base does not match this config class.""" + expected_base = cls.model_fields["base"].default + recognized_base = cls._get_base_or_raise(mod) + if expected_base is not recognized_base: + raise NotAMatchError(f"base is {recognized_base}, not {expected_base}") + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType: + config_dict = get_config_dict_or_raise(common_config_paths(mod.path)) + + if config_dict.get("_class_name") == "FluxControlNetModel": + return BaseModelType.Flux + + dimension = config_dict.get("cross_attention_dim") + + match dimension: + case 768: + return BaseModelType.StableDiffusion1 + case 1024: + # No obvious way to distinguish between sd2-base and sd2-768, but we don't really differentiate them + # anyway. + return BaseModelType.StableDiffusion2 + case 2048: + return BaseModelType.StableDiffusionXL + case _: + raise NotAMatchError(f"unrecognized cross_attention_dim {dimension}") + + +class ControlNet_Diffusers_SD1_Config(ControlNet_Diffusers_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion1] = Field(default=BaseModelType.StableDiffusion1) + + +class ControlNet_Diffusers_SD2_Config(ControlNet_Diffusers_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion2] = Field(default=BaseModelType.StableDiffusion2) + + +class ControlNet_Diffusers_SDXL_Config(ControlNet_Diffusers_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL) + + +class ControlNet_Diffusers_FLUX_Config(ControlNet_Diffusers_Config_Base, Config_Base): + base: Literal[BaseModelType.Flux] = Field(default=BaseModelType.Flux) + + +class ControlNet_Checkpoint_Config_Base(Checkpoint_Config_Base): + """Model config for ControlNet models (diffusers version).""" + + type: Literal[ModelType.ControlNet] = Field(default=ModelType.ControlNet) + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + default_settings: ControlAdapterDefaultSettings | None = Field(None) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_controlnet(mod) + + cls._validate_base(mod) + + return cls(**override_fields) + + @classmethod + def _validate_base(cls, mod: ModelOnDisk) -> None: + """Raise `NotAMatch` if the model base does not match this config class.""" + expected_base = cls.model_fields["base"].default + recognized_base = cls._get_base_or_raise(mod) + if expected_base is not recognized_base: + raise NotAMatchError(f"base is {recognized_base}, not {expected_base}") + + @classmethod + def _validate_looks_like_controlnet(cls, mod: ModelOnDisk) -> None: + if not state_dict_has_any_keys_starting_with( + mod.load_state_dict(), + { + "controlnet", + "control_model", + "input_blocks", + # XLabs FLUX ControlNet models have keys starting with "controlnet_blocks." + # For example: https://huggingface.co/XLabs-AI/flux-controlnet-collections/blob/86ab1e915a389d5857135c00e0d350e9e38a9048/flux-canny-controlnet_v2.safetensors + # TODO(ryand): This is very fragile. XLabs FLUX ControlNet models also contain keys starting with + # "double_blocks.", which we check for above. But, I'm afraid to modify this logic because it is so + # delicate. + "controlnet_blocks", + }, + ): + raise NotAMatchError("state dict does not look like a ControlNet checkpoint") + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType: + state_dict = mod.load_state_dict() + + if is_state_dict_xlabs_controlnet(state_dict) or is_state_dict_instantx_controlnet(state_dict): + # TODO(ryand): Should I distinguish between XLabs, InstantX and other ControlNet models by implementing + # get_format()? + return BaseModelType.Flux + + for key in ( + "control_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight", + "controlnet_mid_block.bias", + "input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight", + "down_blocks.1.attentions.0.transformer_blocks.0.attn2.to_k.weight", + ): + if key not in state_dict: + continue + width = state_dict[key].shape[-1] + match width: + case 768: + return BaseModelType.StableDiffusion1 + case 1024: + return BaseModelType.StableDiffusion2 + case 2048: + return BaseModelType.StableDiffusionXL + case 1280: + return BaseModelType.StableDiffusionXL + case _: + pass + + raise NotAMatchError("unable to determine base type from state dict") + + +class ControlNet_Checkpoint_SD1_Config(ControlNet_Checkpoint_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion1] = Field(default=BaseModelType.StableDiffusion1) + + +class ControlNet_Checkpoint_SD2_Config(ControlNet_Checkpoint_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion2] = Field(default=BaseModelType.StableDiffusion2) + + +class ControlNet_Checkpoint_SDXL_Config(ControlNet_Checkpoint_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL) + + +class ControlNet_Checkpoint_FLUX_Config(ControlNet_Checkpoint_Config_Base, Config_Base): + base: Literal[BaseModelType.Flux] = Field(default=BaseModelType.Flux) + + +def _has_z_image_control_keys(state_dict: dict) -> bool: + """Check if state dict contains Z-Image Control specific keys.""" + z_image_control_keys = {"control_layers", "control_all_x_embedder", "control_noise_refiner"} + for key in state_dict.keys(): + if isinstance(key, str): + prefix = key.split(".")[0] + if prefix in z_image_control_keys: + return True + return False + + +class ControlNet_Checkpoint_ZImage_Config(Checkpoint_Config_Base, Config_Base): + """Model config for Z-Image Control adapter models (Safetensors checkpoint). + + Z-Image Control models are standalone adapters containing only the control layers + (control_layers, control_all_x_embedder, control_noise_refiner) that extend + the base Z-Image transformer with spatial conditioning capabilities. + + Supports: Canny, HED, Depth, Pose, MLSD. + Recommended control_context_scale: 0.65-0.80. + """ + + type: Literal[ModelType.ControlNet] = Field(default=ModelType.ControlNet) + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + base: Literal[BaseModelType.ZImage] = Field(default=BaseModelType.ZImage) + default_settings: ControlAdapterDefaultSettings | None = Field(None) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_z_image_control(mod) + + return cls(**override_fields) + + @classmethod + def _validate_looks_like_z_image_control(cls, mod: ModelOnDisk) -> None: + state_dict = mod.load_state_dict() + if not _has_z_image_control_keys(state_dict): + raise NotAMatchError("state dict does not look like a Z-Image Control model") diff --git a/invokeai/backend/model_manager/configs/external_api.py b/invokeai/backend/model_manager/configs/external_api.py new file mode 100644 index 00000000000..da58cba410f --- /dev/null +++ b/invokeai/backend/model_manager/configs/external_api.py @@ -0,0 +1,113 @@ +from typing import Literal, Self + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from invokeai.backend.model_manager.configs.base import Config_Base +from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelSourceType, ModelType + +ExternalGenerationMode = Literal["txt2img", "img2img", "inpaint"] +ExternalMaskFormat = Literal["alpha", "binary", "none"] +ExternalPanelControlName = Literal["reference_images", "dimensions", "seed"] + + +class ExternalImageSize(BaseModel): + width: int = Field(gt=0) + height: int = Field(gt=0) + + model_config = ConfigDict(extra="forbid") + + +class ExternalResolutionPreset(BaseModel): + label: str = Field(min_length=1, description="Display label, e.g. '1:1 (1K)'") + aspect_ratio: str = Field(min_length=1, description="Aspect ratio string, e.g. '1:1'") + image_size: str = Field(min_length=1, description="Image size preset, e.g. '1K'") + width: int = Field(gt=0) + height: int = Field(gt=0) + + model_config = ConfigDict(extra="forbid") + + +class ExternalModelCapabilities(BaseModel): + modes: list[ExternalGenerationMode] = Field(default_factory=lambda: ["txt2img"]) + supports_reference_images: bool = Field(default=False) + supports_negative_prompt: bool = Field(default=True) + supports_seed: bool = Field(default=False) + supports_guidance: bool = Field(default=False) + supports_steps: bool = Field(default=False) + max_images_per_request: int | None = Field(default=None, gt=0) + max_image_size: ExternalImageSize | None = Field(default=None) + allowed_aspect_ratios: list[str] | None = Field(default=None) + aspect_ratio_sizes: dict[str, ExternalImageSize] | None = Field(default=None) + resolution_presets: list[ExternalResolutionPreset] | None = Field(default=None) + max_reference_images: int | None = Field(default=None, gt=0) + mask_format: ExternalMaskFormat = Field(default="none") + input_image_required_for: list[ExternalGenerationMode] | None = Field(default=None) + + model_config = ConfigDict(extra="forbid") + + +class ExternalApiModelDefaultSettings(BaseModel): + width: int | None = Field(default=None, gt=0) + height: int | None = Field(default=None, gt=0) + num_images: int | None = Field(default=None, gt=0) + + model_config = ConfigDict(extra="forbid") + + +class ExternalModelPanelControl(BaseModel): + name: ExternalPanelControlName + slider_min: float | None = Field(default=None) + slider_max: float | None = Field(default=None) + number_input_min: float | None = Field(default=None) + number_input_max: float | None = Field(default=None) + fine_step: float | None = Field(default=None) + coarse_step: float | None = Field(default=None) + marks: list[float] | None = Field(default=None) + + model_config = ConfigDict(extra="forbid") + + +class ExternalModelPanelSchema(BaseModel): + prompts: list[ExternalModelPanelControl] = Field(default_factory=list) + image: list[ExternalModelPanelControl] = Field(default_factory=list) + generation: list[ExternalModelPanelControl] = Field(default_factory=list) + + model_config = ConfigDict(extra="forbid") + + +class ExternalApiModelConfig(Config_Base): + base: Literal[BaseModelType.External] = Field(default=BaseModelType.External) + type: Literal[ModelType.ExternalImageGenerator] = Field(default=ModelType.ExternalImageGenerator) + format: Literal[ModelFormat.ExternalApi] = Field(default=ModelFormat.ExternalApi) + + provider_id: str = Field(min_length=1, description="External provider ID") + provider_model_id: str = Field(min_length=1, description="Provider-specific model ID") + capabilities: ExternalModelCapabilities = Field(description="Provider capability matrix") + default_settings: ExternalApiModelDefaultSettings | None = Field(default=None) + panel_schema: ExternalModelPanelSchema | None = Field(default=None) + tags: list[str] | None = Field(default=None) + is_default: bool = Field(default=False) + + source_type: ModelSourceType = Field(default=ModelSourceType.External) + path: str = Field(default="") + source: str = Field(default="") + hash: str = Field(default="") + file_size: int = Field(default=0, ge=0) + + model_config = ConfigDict(extra="forbid") + + @model_validator(mode="after") + def _populate_external_fields(self) -> "ExternalApiModelConfig": + if not self.path: + self.path = f"external://{self.provider_id}/{self.provider_model_id}" + if not self.source: + self.source = self.path + if not self.hash: + self.hash = f"external:{self.provider_id}:{self.provider_model_id}" + return self + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, object]) -> Self: + raise NotAMatchError("external API models are not probed from disk") diff --git a/invokeai/backend/model_manager/configs/factory.py b/invokeai/backend/model_manager/configs/factory.py new file mode 100644 index 00000000000..985cb982d30 --- /dev/null +++ b/invokeai/backend/model_manager/configs/factory.py @@ -0,0 +1,583 @@ +import logging +from dataclasses import dataclass +from pathlib import Path +from typing import ( + Union, +) + +from pydantic import Discriminator, TypeAdapter, ValidationError +from typing_extensions import Annotated, Any + +from invokeai.app.services.config.config_default import get_config +from invokeai.app.util.misc import uuid_string +from invokeai.backend.model_hash.model_hash import HASHING_ALGORITHMS +from invokeai.backend.model_manager.configs.base import Config_Base +from invokeai.backend.model_manager.configs.clip_embed import CLIPEmbed_Diffusers_G_Config, CLIPEmbed_Diffusers_L_Config +from invokeai.backend.model_manager.configs.clip_vision import CLIPVision_Diffusers_Config +from invokeai.backend.model_manager.configs.controlnet import ( + ControlAdapterDefaultSettings, + ControlNet_Checkpoint_FLUX_Config, + ControlNet_Checkpoint_SD1_Config, + ControlNet_Checkpoint_SD2_Config, + ControlNet_Checkpoint_SDXL_Config, + ControlNet_Checkpoint_ZImage_Config, + ControlNet_Diffusers_FLUX_Config, + ControlNet_Diffusers_SD1_Config, + ControlNet_Diffusers_SD2_Config, + ControlNet_Diffusers_SDXL_Config, +) +from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig +from invokeai.backend.model_manager.configs.flux_redux import FLUXRedux_Checkpoint_Config +from invokeai.backend.model_manager.configs.identification_utils import NotAMatchError +from invokeai.backend.model_manager.configs.ip_adapter import ( + IPAdapter_Checkpoint_FLUX_Config, + IPAdapter_Checkpoint_SD1_Config, + IPAdapter_Checkpoint_SD2_Config, + IPAdapter_Checkpoint_SDXL_Config, + IPAdapter_InvokeAI_SD1_Config, + IPAdapter_InvokeAI_SD2_Config, + IPAdapter_InvokeAI_SDXL_Config, +) +from invokeai.backend.model_manager.configs.llava_onevision import LlavaOnevision_Diffusers_Config +from invokeai.backend.model_manager.configs.lora import ( + ControlLoRA_LyCORIS_FLUX_Config, + LoRA_Diffusers_Flux2_Config, + LoRA_Diffusers_FLUX_Config, + LoRA_Diffusers_SD1_Config, + LoRA_Diffusers_SD2_Config, + LoRA_Diffusers_SDXL_Config, + LoRA_Diffusers_ZImage_Config, + LoRA_LyCORIS_Anima_Config, + LoRA_LyCORIS_Flux2_Config, + LoRA_LyCORIS_FLUX_Config, + LoRA_LyCORIS_QwenImage_Config, + LoRA_LyCORIS_SD1_Config, + LoRA_LyCORIS_SD2_Config, + LoRA_LyCORIS_SDXL_Config, + LoRA_LyCORIS_ZImage_Config, + LoRA_OMI_FLUX_Config, + LoRA_OMI_SDXL_Config, + LoraModelDefaultSettings, +) +from invokeai.backend.model_manager.configs.main import ( + Main_BnBNF4_FLUX_Config, + Main_Checkpoint_Anima_Config, + Main_Checkpoint_Flux2_Config, + Main_Checkpoint_FLUX_Config, + Main_Checkpoint_SD1_Config, + Main_Checkpoint_SD2_Config, + Main_Checkpoint_SDXL_Config, + Main_Checkpoint_SDXLRefiner_Config, + Main_Checkpoint_ZImage_Config, + Main_Diffusers_CogView4_Config, + Main_Diffusers_Flux2_Config, + Main_Diffusers_FLUX_Config, + Main_Diffusers_QwenImage_Config, + Main_Diffusers_SD1_Config, + Main_Diffusers_SD2_Config, + Main_Diffusers_SD3_Config, + Main_Diffusers_SDXL_Config, + Main_Diffusers_SDXLRefiner_Config, + Main_Diffusers_ZImage_Config, + Main_GGUF_Flux2_Config, + Main_GGUF_FLUX_Config, + Main_GGUF_QwenImage_Config, + Main_GGUF_ZImage_Config, + MainModelDefaultSettings, +) +from invokeai.backend.model_manager.configs.qwen3_encoder import ( + Qwen3Encoder_Checkpoint_Config, + Qwen3Encoder_GGUF_Config, + Qwen3Encoder_Qwen3Encoder_Config, +) +from invokeai.backend.model_manager.configs.qwen_vl_encoder import ( + QwenVLEncoder_Checkpoint_Config, + QwenVLEncoder_Diffusers_Config, +) +from invokeai.backend.model_manager.configs.siglip import SigLIP_Diffusers_Config +from invokeai.backend.model_manager.configs.spandrel import Spandrel_Checkpoint_Config +from invokeai.backend.model_manager.configs.t2i_adapter import ( + T2IAdapter_Diffusers_SD1_Config, + T2IAdapter_Diffusers_SDXL_Config, +) +from invokeai.backend.model_manager.configs.t5_encoder import T5Encoder_BnBLLMint8_Config, T5Encoder_T5Encoder_Config +from invokeai.backend.model_manager.configs.text_llm import TextLLM_Diffusers_Config +from invokeai.backend.model_manager.configs.textual_inversion import ( + TI_File_SD1_Config, + TI_File_SD2_Config, + TI_File_SDXL_Config, + TI_Folder_SD1_Config, + TI_Folder_SD2_Config, + TI_Folder_SDXL_Config, +) +from invokeai.backend.model_manager.configs.unknown import Unknown_Config +from invokeai.backend.model_manager.configs.vae import ( + VAE_Checkpoint_Anima_Config, + VAE_Checkpoint_Flux2_Config, + VAE_Checkpoint_FLUX_Config, + VAE_Checkpoint_QwenImage_Config, + VAE_Checkpoint_SD1_Config, + VAE_Checkpoint_SD2_Config, + VAE_Checkpoint_SDXL_Config, + VAE_Diffusers_Flux2_Config, + VAE_Diffusers_SD1_Config, + VAE_Diffusers_SDXL_Config, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + ModelFormat, + ModelSourceType, + ModelType, + variant_type_adapter, +) + +logger = logging.getLogger(__name__) +app_config = get_config() + +# Known model file extensions for sanity checking +_MODEL_EXTENSIONS = { + ".safetensors", + ".ckpt", + ".pt", + ".pth", + ".bin", + ".gguf", + ".onnx", +} + +# Known config file names for diffusers/transformers models +_CONFIG_FILES = { + "model_index.json", + "config.json", +} + +# Maximum number of files in a directory to be considered a model +_MAX_FILES_IN_MODEL_DIR = 50 + +# Maximum depth to search for model files in directories +_MAX_SEARCH_DEPTH = 2 + + +# The types are listed explicitly because IDEs/LSPs can't identify the correct types +# when AnyModelConfig is constructed dynamically using ModelConfigBase.all_config_classes +AnyModelConfig = Annotated[ + Union[ + # Main (Pipeline) - diffusers format + Annotated[Main_Diffusers_SD1_Config, Main_Diffusers_SD1_Config.get_tag()], + Annotated[Main_Diffusers_SD2_Config, Main_Diffusers_SD2_Config.get_tag()], + Annotated[Main_Diffusers_SDXL_Config, Main_Diffusers_SDXL_Config.get_tag()], + Annotated[Main_Diffusers_SDXLRefiner_Config, Main_Diffusers_SDXLRefiner_Config.get_tag()], + Annotated[Main_Diffusers_SD3_Config, Main_Diffusers_SD3_Config.get_tag()], + Annotated[Main_Diffusers_FLUX_Config, Main_Diffusers_FLUX_Config.get_tag()], + Annotated[Main_Diffusers_Flux2_Config, Main_Diffusers_Flux2_Config.get_tag()], + Annotated[Main_Diffusers_CogView4_Config, Main_Diffusers_CogView4_Config.get_tag()], + Annotated[Main_Diffusers_QwenImage_Config, Main_Diffusers_QwenImage_Config.get_tag()], + Annotated[Main_Diffusers_ZImage_Config, Main_Diffusers_ZImage_Config.get_tag()], + # Main (Pipeline) - checkpoint format + # IMPORTANT: FLUX.2 must be checked BEFORE FLUX.1 because FLUX.2 has specific validation + # that will reject FLUX.1 models, but FLUX.1 validation may incorrectly match FLUX.2 models + Annotated[Main_Checkpoint_SD1_Config, Main_Checkpoint_SD1_Config.get_tag()], + Annotated[Main_Checkpoint_SD2_Config, Main_Checkpoint_SD2_Config.get_tag()], + Annotated[Main_Checkpoint_SDXL_Config, Main_Checkpoint_SDXL_Config.get_tag()], + Annotated[Main_Checkpoint_SDXLRefiner_Config, Main_Checkpoint_SDXLRefiner_Config.get_tag()], + Annotated[Main_Checkpoint_Flux2_Config, Main_Checkpoint_Flux2_Config.get_tag()], + Annotated[Main_Checkpoint_FLUX_Config, Main_Checkpoint_FLUX_Config.get_tag()], + Annotated[Main_Checkpoint_ZImage_Config, Main_Checkpoint_ZImage_Config.get_tag()], + Annotated[Main_Checkpoint_Anima_Config, Main_Checkpoint_Anima_Config.get_tag()], + # Main (Pipeline) - quantized formats + # IMPORTANT: FLUX.2 must be checked BEFORE FLUX.1 because FLUX.2 has specific validation + # that will reject FLUX.1 models, but FLUX.1 validation may incorrectly match FLUX.2 models + Annotated[Main_BnBNF4_FLUX_Config, Main_BnBNF4_FLUX_Config.get_tag()], + Annotated[Main_GGUF_Flux2_Config, Main_GGUF_Flux2_Config.get_tag()], + Annotated[Main_GGUF_FLUX_Config, Main_GGUF_FLUX_Config.get_tag()], + Annotated[Main_GGUF_QwenImage_Config, Main_GGUF_QwenImage_Config.get_tag()], + Annotated[Main_GGUF_ZImage_Config, Main_GGUF_ZImage_Config.get_tag()], + # VAE - checkpoint format + Annotated[VAE_Checkpoint_SD1_Config, VAE_Checkpoint_SD1_Config.get_tag()], + Annotated[VAE_Checkpoint_SD2_Config, VAE_Checkpoint_SD2_Config.get_tag()], + Annotated[VAE_Checkpoint_SDXL_Config, VAE_Checkpoint_SDXL_Config.get_tag()], + Annotated[VAE_Checkpoint_FLUX_Config, VAE_Checkpoint_FLUX_Config.get_tag()], + Annotated[VAE_Checkpoint_Flux2_Config, VAE_Checkpoint_Flux2_Config.get_tag()], + Annotated[VAE_Checkpoint_QwenImage_Config, VAE_Checkpoint_QwenImage_Config.get_tag()], + Annotated[VAE_Checkpoint_Anima_Config, VAE_Checkpoint_Anima_Config.get_tag()], + # VAE - diffusers format + Annotated[VAE_Diffusers_SD1_Config, VAE_Diffusers_SD1_Config.get_tag()], + Annotated[VAE_Diffusers_SDXL_Config, VAE_Diffusers_SDXL_Config.get_tag()], + Annotated[VAE_Diffusers_Flux2_Config, VAE_Diffusers_Flux2_Config.get_tag()], + # ControlNet - checkpoint format + Annotated[ControlNet_Checkpoint_SD1_Config, ControlNet_Checkpoint_SD1_Config.get_tag()], + Annotated[ControlNet_Checkpoint_SD2_Config, ControlNet_Checkpoint_SD2_Config.get_tag()], + Annotated[ControlNet_Checkpoint_SDXL_Config, ControlNet_Checkpoint_SDXL_Config.get_tag()], + Annotated[ControlNet_Checkpoint_FLUX_Config, ControlNet_Checkpoint_FLUX_Config.get_tag()], + Annotated[ControlNet_Checkpoint_ZImage_Config, ControlNet_Checkpoint_ZImage_Config.get_tag()], + # ControlNet - diffusers format + Annotated[ControlNet_Diffusers_SD1_Config, ControlNet_Diffusers_SD1_Config.get_tag()], + Annotated[ControlNet_Diffusers_SD2_Config, ControlNet_Diffusers_SD2_Config.get_tag()], + Annotated[ControlNet_Diffusers_SDXL_Config, ControlNet_Diffusers_SDXL_Config.get_tag()], + Annotated[ControlNet_Diffusers_FLUX_Config, ControlNet_Diffusers_FLUX_Config.get_tag()], + # LoRA - LyCORIS format + # IMPORTANT: FLUX.2 must be checked BEFORE FLUX.1 because FLUX.2 has specific validation + # that will reject FLUX.1 models, but FLUX.1 validation may incorrectly match FLUX.2 models + Annotated[LoRA_LyCORIS_SD1_Config, LoRA_LyCORIS_SD1_Config.get_tag()], + Annotated[LoRA_LyCORIS_SD2_Config, LoRA_LyCORIS_SD2_Config.get_tag()], + Annotated[LoRA_LyCORIS_SDXL_Config, LoRA_LyCORIS_SDXL_Config.get_tag()], + Annotated[LoRA_LyCORIS_Flux2_Config, LoRA_LyCORIS_Flux2_Config.get_tag()], + Annotated[LoRA_LyCORIS_FLUX_Config, LoRA_LyCORIS_FLUX_Config.get_tag()], + Annotated[LoRA_LyCORIS_ZImage_Config, LoRA_LyCORIS_ZImage_Config.get_tag()], + Annotated[LoRA_LyCORIS_QwenImage_Config, LoRA_LyCORIS_QwenImage_Config.get_tag()], + Annotated[LoRA_LyCORIS_Anima_Config, LoRA_LyCORIS_Anima_Config.get_tag()], + # LoRA - OMI format + Annotated[LoRA_OMI_SDXL_Config, LoRA_OMI_SDXL_Config.get_tag()], + Annotated[LoRA_OMI_FLUX_Config, LoRA_OMI_FLUX_Config.get_tag()], + # LoRA - diffusers format + # IMPORTANT: FLUX.2 must be checked BEFORE FLUX.1 because FLUX.2 has specific validation + # that will reject FLUX.1 models, but FLUX.1 validation may incorrectly match FLUX.2 models + Annotated[LoRA_Diffusers_SD1_Config, LoRA_Diffusers_SD1_Config.get_tag()], + Annotated[LoRA_Diffusers_SD2_Config, LoRA_Diffusers_SD2_Config.get_tag()], + Annotated[LoRA_Diffusers_SDXL_Config, LoRA_Diffusers_SDXL_Config.get_tag()], + Annotated[LoRA_Diffusers_Flux2_Config, LoRA_Diffusers_Flux2_Config.get_tag()], + Annotated[LoRA_Diffusers_FLUX_Config, LoRA_Diffusers_FLUX_Config.get_tag()], + Annotated[LoRA_Diffusers_ZImage_Config, LoRA_Diffusers_ZImage_Config.get_tag()], + # ControlLoRA - diffusers format + Annotated[ControlLoRA_LyCORIS_FLUX_Config, ControlLoRA_LyCORIS_FLUX_Config.get_tag()], + # T5 Encoder - all formats + Annotated[T5Encoder_T5Encoder_Config, T5Encoder_T5Encoder_Config.get_tag()], + Annotated[T5Encoder_BnBLLMint8_Config, T5Encoder_BnBLLMint8_Config.get_tag()], + # Qwen3 Encoder + Annotated[Qwen3Encoder_Qwen3Encoder_Config, Qwen3Encoder_Qwen3Encoder_Config.get_tag()], + Annotated[Qwen3Encoder_Checkpoint_Config, Qwen3Encoder_Checkpoint_Config.get_tag()], + Annotated[Qwen3Encoder_GGUF_Config, Qwen3Encoder_GGUF_Config.get_tag()], + # Qwen VL Encoder (Qwen2.5-VL multimodal encoder for Qwen Image) + Annotated[QwenVLEncoder_Diffusers_Config, QwenVLEncoder_Diffusers_Config.get_tag()], + Annotated[QwenVLEncoder_Checkpoint_Config, QwenVLEncoder_Checkpoint_Config.get_tag()], + # TI - file format + Annotated[TI_File_SD1_Config, TI_File_SD1_Config.get_tag()], + Annotated[TI_File_SD2_Config, TI_File_SD2_Config.get_tag()], + Annotated[TI_File_SDXL_Config, TI_File_SDXL_Config.get_tag()], + # TI - folder format + Annotated[TI_Folder_SD1_Config, TI_Folder_SD1_Config.get_tag()], + Annotated[TI_Folder_SD2_Config, TI_Folder_SD2_Config.get_tag()], + Annotated[TI_Folder_SDXL_Config, TI_Folder_SDXL_Config.get_tag()], + # IP Adapter - InvokeAI format + Annotated[IPAdapter_InvokeAI_SD1_Config, IPAdapter_InvokeAI_SD1_Config.get_tag()], + Annotated[IPAdapter_InvokeAI_SD2_Config, IPAdapter_InvokeAI_SD2_Config.get_tag()], + Annotated[IPAdapter_InvokeAI_SDXL_Config, IPAdapter_InvokeAI_SDXL_Config.get_tag()], + # IP Adapter - checkpoint format + Annotated[IPAdapter_Checkpoint_SD1_Config, IPAdapter_Checkpoint_SD1_Config.get_tag()], + Annotated[IPAdapter_Checkpoint_SD2_Config, IPAdapter_Checkpoint_SD2_Config.get_tag()], + Annotated[IPAdapter_Checkpoint_SDXL_Config, IPAdapter_Checkpoint_SDXL_Config.get_tag()], + Annotated[IPAdapter_Checkpoint_FLUX_Config, IPAdapter_Checkpoint_FLUX_Config.get_tag()], + # T2I Adapter - diffusers format + Annotated[T2IAdapter_Diffusers_SD1_Config, T2IAdapter_Diffusers_SD1_Config.get_tag()], + Annotated[T2IAdapter_Diffusers_SDXL_Config, T2IAdapter_Diffusers_SDXL_Config.get_tag()], + # Misc models + Annotated[Spandrel_Checkpoint_Config, Spandrel_Checkpoint_Config.get_tag()], + Annotated[CLIPEmbed_Diffusers_G_Config, CLIPEmbed_Diffusers_G_Config.get_tag()], + Annotated[CLIPEmbed_Diffusers_L_Config, CLIPEmbed_Diffusers_L_Config.get_tag()], + Annotated[CLIPVision_Diffusers_Config, CLIPVision_Diffusers_Config.get_tag()], + Annotated[SigLIP_Diffusers_Config, SigLIP_Diffusers_Config.get_tag()], + Annotated[FLUXRedux_Checkpoint_Config, FLUXRedux_Checkpoint_Config.get_tag()], + Annotated[LlavaOnevision_Diffusers_Config, LlavaOnevision_Diffusers_Config.get_tag()], + Annotated[TextLLM_Diffusers_Config, TextLLM_Diffusers_Config.get_tag()], + Annotated[ExternalApiModelConfig, ExternalApiModelConfig.get_tag()], + # Unknown model (fallback) + Annotated[Unknown_Config, Unknown_Config.get_tag()], + ], + Discriminator(Config_Base.get_model_discriminator_value), +] + +AnyModelConfigValidator = TypeAdapter[AnyModelConfig](AnyModelConfig) +"""Pydantic TypeAdapter for the AnyModelConfig union, used for parsing and validation. + +If you need to parse/validate a dict or JSON into an AnyModelConfig, you should probably use +ModelConfigFactory.from_dict or ModelConfigFactory.from_json instead as they may implement +additional logic in the future. +""" + + +@dataclass +class ModelClassificationResult: + """Result of attempting to classify a model on disk into a specific model config. + + Attributes: + match: The best matching model config, or None if no match was found. + results: A mapping of model config class names to either an instance of that class (if it matched) + or an Exception (if it didn't match or an error occurred during matching). + """ + + config: AnyModelConfig | None + details: dict[str, AnyModelConfig | Exception] + + @property + def all_matches(self) -> list[AnyModelConfig]: + """Returns a list of all matching model configs found.""" + return [r for r in self.details.values() if isinstance(r, Config_Base)] + + @property + def match_count(self) -> int: + """Returns the number of matching model configs found.""" + return len(self.all_matches) + + +class ModelConfigFactory: + @staticmethod + def from_dict(fields: dict[str, Any]) -> AnyModelConfig: + """Return the appropriate config object from raw dict values.""" + model = AnyModelConfigValidator.validate_python(fields) + return model + + @staticmethod + def from_json(json: str | bytes | bytearray) -> AnyModelConfig: + """Return the appropriate config object from json.""" + model = AnyModelConfigValidator.validate_json(json) + return model + + @staticmethod + def build_common_fields( + mod: ModelOnDisk, + override_fields: dict[str, Any] | None = None, + ) -> dict[str, Any]: + """Builds the common fields for all model configs. + + Args: + mod: The model on disk to extract fields from. + overrides: A optional dictionary of fields to override. These fields will take precedence over the values + extracted from the model on disk. + + - Casts string fields to their Enum types. + - Does not validate the fields against the model config schema. + """ + + _overrides: dict[str, Any] = override_fields or {} + fields: dict[str, Any] = {} + + if "type" in _overrides: + fields["type"] = ModelType(_overrides["type"]) + + if "format" in _overrides: + fields["format"] = ModelFormat(_overrides["format"]) + + if "base" in _overrides: + fields["base"] = BaseModelType(_overrides["base"]) + + if "source_type" in _overrides: + fields["source_type"] = ModelSourceType(_overrides["source_type"]) + + if "variant" in _overrides: + fields["variant"] = variant_type_adapter.validate_strings(_overrides["variant"]) + + fields["path"] = mod.path.as_posix() + fields["source"] = _overrides.get("source") or fields["path"] + fields["source_type"] = _overrides.get("source_type") or ModelSourceType.Path + fields["name"] = _overrides.get("name") or mod.name + fields["hash"] = _overrides.get("hash") or mod.hash() + fields["key"] = _overrides.get("key") or uuid_string() + fields["description"] = _overrides.get("description") + fields["file_size"] = _overrides.get("file_size") or mod.size() + + return fields + + @staticmethod + def _validate_path_looks_like_model(path: Path) -> None: + """Perform basic sanity checks to ensure a path looks like a model. + + This prevents wasting time trying to identify obviously non-model paths like + home directories or downloads folders. Raises RuntimeError if the path doesn't + pass basic checks. + + Args: + path: The path to validate + + Raises: + ValueError: If the path doesn't look like a model + """ + if path.is_file(): + # For files, just check the extension + if path.suffix.lower() not in _MODEL_EXTENSIONS: + raise ValueError( + f"File extension {path.suffix} is not a recognized model format. " + f"Expected one of: {', '.join(sorted(_MODEL_EXTENSIONS))}" + ) + else: + # For directories, do a quick file count check with early exit + total_files = 0 + # Ignore hidden files and directories + paths_to_check = ( + p + for p in path.rglob("*") + if not p.name.startswith(".") and not any(part.startswith(".") for part in p.parts) + ) + for item in paths_to_check: + if item.is_file(): + total_files += 1 + if total_files > _MAX_FILES_IN_MODEL_DIR: + raise ValueError( + f"Directory contains more than {_MAX_FILES_IN_MODEL_DIR} files. " + "This looks like a general-purpose directory rather than a model. " + "Please provide a path to a specific model file or model directory." + ) + + # Check if it has config files at root (diffusers/transformers marker) + has_root_config = any((path / config).exists() for config in _CONFIG_FILES) + + if has_root_config: + # Has a config file, looks like a valid model directory + return + + # Otherwise, search for model files within depth limit + def find_model_files(current_path: Path, depth: int) -> bool: + if depth > _MAX_SEARCH_DEPTH: + return False + try: + for item in current_path.iterdir(): + if item.is_file() and item.suffix.lower() in _MODEL_EXTENSIONS: + return True + elif item.is_dir() and find_model_files(item, depth + 1): + return True + except PermissionError: + pass + return False + + if not find_model_files(path, 0): + raise ValueError( + f"No model files or config files found in directory {path}. " + f"Expected to find model files with extensions: {', '.join(sorted(_MODEL_EXTENSIONS))} " + f"or config files: {', '.join(sorted(_CONFIG_FILES))}" + ) + + @staticmethod + def matches_sort_key(m: AnyModelConfig) -> int: + """Sort key function to prioritize model config matches in case of multiple matches.""" + + # It is possible that we have multiple matches. We need to prioritize them. + + # Known cases where multiple matches can occur: + # - SD main models can look like a LoRA when they have merged in LoRA weights. Prefer the main model. + # - SD main models in diffusers format can look like a CLIP Embed; they have a text_encoder folder with + # a config.json file. Prefer the main model. + + # Given the above cases, we can prioritize the matches by type. If we find more cases, we may need a more + # sophisticated approach. + match m.type: + case ModelType.Main: + return 0 + case ModelType.LoRA: + return 1 + case ModelType.CLIPEmbed: + return 2 + case _: + return 3 + + @staticmethod + def from_model_on_disk( + mod: str | Path | ModelOnDisk, + override_fields: dict[str, Any] | None = None, + hash_algo: HASHING_ALGORITHMS = "blake3_single", + allow_unknown: bool = True, + ) -> ModelClassificationResult: + """Classify a model on disk and return the best matching model config. + + Args: + mod: The model on disk to classify. Can be a path (str or Path) or a ModelOnDisk instance. + override_fields: Optional dictionary of fields to override. These fields will take precedence + over the values extracted from the model on disk, but this cannot force a match if the + model on disk doesn't actually match the config class. + hash_algo: The hashing algorithm to use when computing the model hash if needed. + + Returns: + A ModelClassificationResult containing the best matching model config (or None if no match) + and a mapping of all attempted model config classes to either an instance of that class (if it matched) + or an Exception (if it didn't match or an error occurred during matching). + + Raises: + ValueError: If the provided path doesn't look like a model. + """ + if isinstance(mod, Path | str): + mod = ModelOnDisk(Path(mod), hash_algo) + + # Perform basic sanity checks before attempting any config matching + # This rejects obviously non-model paths early, saving time + ModelConfigFactory._validate_path_looks_like_model(mod.path) + + # We will always need these fields to build any model config. + fields = ModelConfigFactory.build_common_fields(mod, override_fields) + + # Store results as a mapping of config class to either an instance of that class or an exception + # that was raised when trying to build it. + details: dict[str, AnyModelConfig | Exception] = {} + + # Try to build an instance of each model config class that uses the classify API. + # Each class will either return an instance of itself or raise NotAMatch if it doesn't match. + # Other exceptions may be raised if something unexpected happens during matching or building. + for candidate_class in filter(lambda x: x is not Unknown_Config, Config_Base.CONFIG_CLASSES): + candidate_name = candidate_class.__name__ + try: + # Technically, from_model_on_disk returns a Config_Base, but in practice it will always be a member of + # the AnyModelConfig union. + details[candidate_name] = candidate_class.from_model_on_disk(mod, fields) # type: ignore + except NotAMatchError as e: + # This means the model didn't match this config class. It's not an error, just no match. + details[candidate_name] = e + except ValidationError as e: + # This means the model matched, but we couldn't create the pydantic model instance for the config. + # Maybe invalid overrides were provided? + details[candidate_name] = e + except Exception as e: + # Some other unexpected error occurred. Store the exception for reporting later. + details[candidate_name] = e + + # Extract just the successful matches + matches = [r for r in details.values() if isinstance(r, Config_Base)] + + if not matches: + if not allow_unknown: + # No matches and we are not allowed to fall back to Unknown_Config + return ModelClassificationResult(config=None, details=details) + else: + # Fall back to Unknown_Config + # This should always succeed as Unknown_Config.from_model_on_disk never raises NotAMatch + config = Unknown_Config.from_model_on_disk(mod, fields) + details[Unknown_Config.__name__] = config + return ModelClassificationResult(config=config, details=details) + + matches.sort(key=ModelConfigFactory.matches_sort_key) + config = matches[0] + + # Now do any post-processing needed for specific model types/bases/etc. + match config.type: + case ModelType.Main: + # Pass variant if available (e.g., for Flux2 models) + variant = getattr(config, "variant", None) + config.default_settings = MainModelDefaultSettings.from_base(config.base, variant) + case ModelType.ControlNet | ModelType.T2IAdapter | ModelType.ControlLoRa: + config.default_settings = ControlAdapterDefaultSettings.from_model_name(config.name) + case ModelType.LoRA: + config.default_settings = LoraModelDefaultSettings() + case _: + pass + + return ModelClassificationResult(config=config, details=details) + + +MODEL_NAME_TO_PREPROCESSOR = { + "canny": "canny_image_processor", + "mlsd": "mlsd_image_processor", + "depth": "depth_anything_image_processor", + "bae": "normalbae_image_processor", + "normal": "normalbae_image_processor", + "sketch": "pidi_image_processor", + "scribble": "lineart_image_processor", + "lineart anime": "lineart_anime_image_processor", + "lineart_anime": "lineart_anime_image_processor", + "lineart": "lineart_image_processor", + "soft": "hed_image_processor", + "softedge": "hed_image_processor", + "hed": "hed_image_processor", + "shuffle": "content_shuffle_image_processor", + "pose": "dw_openpose_image_processor", + "mediapipe": "mediapipe_face_processor", + "pidi": "pidi_image_processor", + "zoe": "zoe_depth_image_processor", + "color": "color_map_image_processor", +} diff --git a/invokeai/backend/model_manager/configs/flux_redux.py b/invokeai/backend/model_manager/configs/flux_redux.py new file mode 100644 index 00000000000..6eb76116fba --- /dev/null +++ b/invokeai/backend/model_manager/configs/flux_redux.py @@ -0,0 +1,40 @@ +from typing import ( + Literal, + Self, +) + +from pydantic import Field +from typing_extensions import Any + +from invokeai.backend.flux.redux.flux_redux_state_dict_utils import is_state_dict_likely_flux_redux +from invokeai.backend.model_manager.configs.base import Config_Base +from invokeai.backend.model_manager.configs.identification_utils import ( + NotAMatchError, + raise_for_override_fields, + raise_if_not_file, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + ModelFormat, + ModelType, +) + + +class FLUXRedux_Checkpoint_Config(Config_Base): + """Model config for FLUX Tools Redux model.""" + + type: Literal[ModelType.FluxRedux] = Field(default=ModelType.FluxRedux) + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + base: Literal[BaseModelType.Flux] = Field(default=BaseModelType.Flux) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + if not is_state_dict_likely_flux_redux(mod.load_state_dict()): + raise NotAMatchError("model does not match FLUX Tools Redux heuristics") + + return cls(**override_fields) diff --git a/invokeai/backend/model_manager/configs/identification_utils.py b/invokeai/backend/model_manager/configs/identification_utils.py new file mode 100644 index 00000000000..ce7d2c792de --- /dev/null +++ b/invokeai/backend/model_manager/configs/identification_utils.py @@ -0,0 +1,206 @@ +import json +from functools import cache +from pathlib import Path + +from pydantic import BaseModel, ValidationError +from pydantic_core import CoreSchema, SchemaValidator +from typing_extensions import Any + +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk + + +class NotAMatchError(Exception): + """Exception for when a model does not match a config class. + + Args: + reason: The reason why the model did not match. + """ + + def __init__(self, reason: str): + super().__init__(reason) + + +def get_config_dict_or_raise(config_path: Path | set[Path]) -> dict[str, Any]: + """Load the diffusers/transformers model config file and return it as a dictionary. The config file is expected + to be in JSON format. + + Args: + config_path: The path to the config file, or a set of paths to try. + + Returns: + The config file as a dictionary. + + Raises: + NotAMatch if the config file is missing or cannot be loaded. + """ + paths_to_check = config_path if isinstance(config_path, set) else {config_path} + + problems: dict[Path, str] = {} + + for p in paths_to_check: + if not p.exists(): + problems[p] = "file does not exist" + continue + + try: + with open(p, "r") as file: + config = json.load(file) + + return config + except Exception as e: + problems[p] = str(e) + continue + + raise NotAMatchError(f"unable to load config file(s): {problems}") + + +def get_class_name_from_config_dict_or_raise(config: Path | set[Path] | dict[str, Any]) -> str: + """Load the diffusers/transformers model config file and return the class name. + + Args: + config_path: The path to the config file, or a set of paths to try. + + Returns: + The class name from the config file. + + Raises: + NotAMatch if the config file is missing or does not contain a valid class name. + """ + + if not isinstance(config, dict): + config = get_config_dict_or_raise(config) + + try: + if "_class_name" in config: + # This is a diffusers-style config + config_class_name = config["_class_name"] + elif "architectures" in config: + # This is a transformers-style config + config_class_name = config["architectures"][0] + else: + raise ValueError("missing _class_name or architectures field") + except Exception as e: + raise NotAMatchError(f"unable to determine class name from config file: {config}") from e + + if not isinstance(config_class_name, str): + raise NotAMatchError(f"_class_name or architectures field is not a string: {config_class_name}") + + return config_class_name + + +def raise_for_class_name(config: Path | set[Path] | dict[str, Any], class_name: str | set[str]) -> None: + """Get the class name from the config file and raise NotAMatch if it is not in the expected set. + + Args: + config_path: The path to the config file, or a set of paths to try. + class_name: The expected class name, or a set of expected class names. + + Raises: + NotAMatch if the class name is not in the expected set. + """ + + class_name = {class_name} if isinstance(class_name, str) else class_name + + actual_class_name = get_class_name_from_config_dict_or_raise(config) + if actual_class_name not in class_name: + raise NotAMatchError(f"invalid class name from config: {actual_class_name}") + + +def raise_for_override_fields(candidate_config_class: type[BaseModel], override_fields: dict[str, Any]) -> None: + """Check if the provided override fields are valid for the config class using pydantic. + + For example, if the candidate config class has a field "base" of type Literal[BaseModelType.StableDiffusion1], and + the override fields contain "base": BaseModelType.Flux, this function will raise NotAMatch. + + Internally, this function extracts the pydantic schema for each individual override field from the candidate config + class and validates the override value against that schema. Post-instantiation validators are not run. + + Args: + candidate_config_class: The config class that is being tested. + override_fields: The override fields provided by the user. + + Raises: + NotAMatch if any override field is invalid for the config class. + """ + for field_name, override_value in override_fields.items(): + if field_name not in candidate_config_class.model_fields: + raise NotAMatchError(f"unknown override field: {field_name}") + try: + PydanticFieldValidator.validate_field(candidate_config_class, field_name, override_value) + except ValidationError as e: + raise NotAMatchError(f"invalid override for field '{field_name}': {e}") from e + + +def raise_if_not_file(mod: ModelOnDisk) -> None: + """Raise NotAMatch if the model path is not a file.""" + if not mod.path.is_file(): + raise NotAMatchError("model path is not a file") + + +def raise_if_not_dir(mod: ModelOnDisk) -> None: + """Raise NotAMatch if the model path is not a directory.""" + if not mod.path.is_dir(): + raise NotAMatchError("model path is not a directory") + + +def state_dict_has_any_keys_exact(state_dict: dict[str | int, Any], keys: str | set[str]) -> bool: + """Returns true if the state dict has any of the specified keys.""" + _keys = {keys} if isinstance(keys, str) else keys + return any(key in state_dict for key in _keys) + + +def state_dict_has_any_keys_starting_with(state_dict: dict[str | int, Any], prefixes: str | set[str]) -> bool: + """Returns true if the state dict has any keys starting with any of the specified prefixes.""" + _prefixes = {prefixes} if isinstance(prefixes, str) else prefixes + return any(any(key.startswith(prefix) for prefix in _prefixes) for key in state_dict.keys() if isinstance(key, str)) + + +def state_dict_has_any_keys_ending_with(state_dict: dict[str | int, Any], suffixes: str | set[str]) -> bool: + """Returns true if the state dict has any keys ending with any of the specified suffixes.""" + _suffixes = {suffixes} if isinstance(suffixes, str) else suffixes + return any(any(key.endswith(suffix) for suffix in _suffixes) for key in state_dict.keys() if isinstance(key, str)) + + +def common_config_paths(path: Path) -> set[Path]: + """Returns common config file paths for models stored in directories.""" + return {path / "config.json", path / "model_index.json"} + + +class PydanticFieldValidator: + """Utility class for validating individual fields of a Pydantic model without instantiating the whole model. + + See: https://github.com/pydantic/pydantic/discussions/7367#discussioncomment-14213144 + """ + + @staticmethod + def find_field_schema(model: type[BaseModel], field_name: str) -> CoreSchema: + """Find the Pydantic core schema for a specific field in a model.""" + schema: CoreSchema = model.__pydantic_core_schema__.copy() + # we shallow copied, be careful not to mutate the original schema! + + assert schema["type"] in ["definitions", "model"] + + # find the field schema + field_schema = schema["schema"] # type: ignore + while "fields" not in field_schema: + field_schema = field_schema["schema"] # type: ignore + + field_schema = field_schema["fields"][field_name]["schema"] # type: ignore + + # if the original schema is a definition schema, replace the model schema with the field schema + if schema["type"] == "definitions": + schema["schema"] = field_schema + return schema + else: + return field_schema + + @cache + @staticmethod + def get_validator(model: type[BaseModel], field_name: str) -> SchemaValidator: + """Get a SchemaValidator for a specific field in a model.""" + return SchemaValidator(PydanticFieldValidator.find_field_schema(model, field_name)) + + @staticmethod + def validate_field(model: type[BaseModel], field_name: str, value: Any) -> Any: + """Validate a value for a specific field in a model.""" + return PydanticFieldValidator.get_validator(model, field_name).validate_python(value) diff --git a/invokeai/backend/model_manager/configs/ip_adapter.py b/invokeai/backend/model_manager/configs/ip_adapter.py new file mode 100644 index 00000000000..ba27f176201 --- /dev/null +++ b/invokeai/backend/model_manager/configs/ip_adapter.py @@ -0,0 +1,180 @@ +from abc import ABC +from typing import ( + Literal, + Self, +) + +from pydantic import BaseModel, Field +from typing_extensions import Any + +from invokeai.backend.flux.ip_adapter.state_dict_utils import is_state_dict_xlabs_ip_adapter +from invokeai.backend.model_manager.configs.base import Config_Base +from invokeai.backend.model_manager.configs.identification_utils import ( + NotAMatchError, + raise_for_override_fields, + raise_if_not_dir, + raise_if_not_file, + state_dict_has_any_keys_starting_with, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + ModelFormat, + ModelType, +) + + +class IPAdapter_Config_Base(ABC, BaseModel): + type: Literal[ModelType.IPAdapter] = Field(default=ModelType.IPAdapter) + + +class IPAdapter_InvokeAI_Config_Base(IPAdapter_Config_Base): + """Model config for IP Adapter diffusers format models.""" + + format: Literal[ModelFormat.InvokeAI] = Field(default=ModelFormat.InvokeAI) + + # TODO(ryand): Should we deprecate this field? From what I can tell, it hasn't been probed correctly for a long + # time. Need to go through the history to make sure I'm understanding this fully. + image_encoder_model_id: str = Field() + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_has_weights_file(mod) + + cls._validate_has_image_encoder_metadata_file(mod) + + cls._validate_base(mod) + + return cls(**override_fields) + + @classmethod + def _validate_base(cls, mod: ModelOnDisk) -> None: + """Raise `NotAMatch` if the model base does not match this config class.""" + expected_base = cls.model_fields["base"].default + recognized_base = cls._get_base_or_raise(mod) + if expected_base is not recognized_base: + raise NotAMatchError(f"base is {recognized_base}, not {expected_base}") + + @classmethod + def _validate_has_weights_file(cls, mod: ModelOnDisk) -> None: + weights_file = mod.path / "ip_adapter.bin" + if not weights_file.exists(): + raise NotAMatchError("missing ip_adapter.bin weights file") + + @classmethod + def _validate_has_image_encoder_metadata_file(cls, mod: ModelOnDisk) -> None: + image_encoder_metadata_file = mod.path / "image_encoder.txt" + if not image_encoder_metadata_file.exists(): + raise NotAMatchError("missing image_encoder.txt metadata file") + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType: + state_dict = mod.load_state_dict() + + try: + cross_attention_dim = state_dict["ip_adapter"]["1.to_k_ip.weight"].shape[-1] + except Exception as e: + raise NotAMatchError(f"unable to determine cross attention dimension: {e}") from e + + match cross_attention_dim: + case 768: + return BaseModelType.StableDiffusion1 + case 1024: + return BaseModelType.StableDiffusion2 + case 2048: + return BaseModelType.StableDiffusionXL + case _: + raise NotAMatchError(f"unrecognized cross attention dimension {cross_attention_dim}") + + +class IPAdapter_InvokeAI_SD1_Config(IPAdapter_InvokeAI_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion1] = Field(default=BaseModelType.StableDiffusion1) + + +class IPAdapter_InvokeAI_SD2_Config(IPAdapter_InvokeAI_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion2] = Field(default=BaseModelType.StableDiffusion2) + + +class IPAdapter_InvokeAI_SDXL_Config(IPAdapter_InvokeAI_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL) + + +class IPAdapter_Checkpoint_Config_Base(IPAdapter_Config_Base): + """Model config for IP Adapter checkpoint format models.""" + + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_ip_adapter(mod) + + cls._validate_base(mod) + + return cls(**override_fields) + + @classmethod + def _validate_base(cls, mod: ModelOnDisk) -> None: + """Raise `NotAMatch` if the model base does not match this config class.""" + expected_base = cls.model_fields["base"].default + recognized_base = cls._get_base_or_raise(mod) + if expected_base is not recognized_base: + raise NotAMatchError(f"base is {recognized_base}, not {expected_base}") + + @classmethod + def _validate_looks_like_ip_adapter(cls, mod: ModelOnDisk) -> None: + if not state_dict_has_any_keys_starting_with( + mod.load_state_dict(), + { + "image_proj.", + "ip_adapter.", + # XLabs FLUX IP-Adapter models have keys startinh with "ip_adapter_proj_model.". + "ip_adapter_proj_model.", + }, + ): + raise NotAMatchError("model does not match Checkpoint IP Adapter heuristics") + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType: + state_dict = mod.load_state_dict() + + if is_state_dict_xlabs_ip_adapter(state_dict): + return BaseModelType.Flux + + try: + cross_attention_dim = state_dict["ip_adapter.1.to_k_ip.weight"].shape[-1] + except Exception as e: + raise NotAMatchError(f"unable to determine cross attention dimension: {e}") from e + + match cross_attention_dim: + case 768: + return BaseModelType.StableDiffusion1 + case 1024: + return BaseModelType.StableDiffusion2 + case 2048: + return BaseModelType.StableDiffusionXL + case _: + raise NotAMatchError(f"unrecognized cross attention dimension {cross_attention_dim}") + + +class IPAdapter_Checkpoint_SD1_Config(IPAdapter_Checkpoint_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion1] = Field(default=BaseModelType.StableDiffusion1) + + +class IPAdapter_Checkpoint_SD2_Config(IPAdapter_Checkpoint_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion2] = Field(default=BaseModelType.StableDiffusion2) + + +class IPAdapter_Checkpoint_SDXL_Config(IPAdapter_Checkpoint_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL) + + +class IPAdapter_Checkpoint_FLUX_Config(IPAdapter_Checkpoint_Config_Base, Config_Base): + base: Literal[BaseModelType.Flux] = Field(default=BaseModelType.Flux) diff --git a/invokeai/backend/model_manager/configs/llava_onevision.py b/invokeai/backend/model_manager/configs/llava_onevision.py new file mode 100644 index 00000000000..8f7ec28e398 --- /dev/null +++ b/invokeai/backend/model_manager/configs/llava_onevision.py @@ -0,0 +1,43 @@ +from typing import ( + Literal, + Self, +) + +from pydantic import Field +from typing_extensions import Any + +from invokeai.backend.model_manager.configs.base import Config_Base, Diffusers_Config_Base +from invokeai.backend.model_manager.configs.identification_utils import ( + common_config_paths, + raise_for_class_name, + raise_for_override_fields, + raise_if_not_dir, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + ModelType, +) + + +class LlavaOnevision_Diffusers_Config(Diffusers_Config_Base, Config_Base): + """Model config for Llava Onevision models.""" + + type: Literal[ModelType.LlavaOnevision] = Field(default=ModelType.LlavaOnevision) + base: Literal[BaseModelType.Any] = Field(default=BaseModelType.Any) + cpu_only: bool | None = Field(default=None, description="Whether this model should run on CPU only") + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + raise_for_class_name( + common_config_paths(mod.path), + { + "LlavaOnevisionForConditionalGeneration", + }, + ) + + return cls(**override_fields) diff --git a/invokeai/backend/model_manager/configs/lora.py b/invokeai/backend/model_manager/configs/lora.py new file mode 100644 index 00000000000..46606a3c0d5 --- /dev/null +++ b/invokeai/backend/model_manager/configs/lora.py @@ -0,0 +1,1069 @@ +from abc import ABC +from pathlib import Path +from typing import ( + Any, + Literal, + Self, +) + +from pydantic import BaseModel, ConfigDict, Field + +from invokeai.backend.model_manager.configs.base import ( + Config_Base, +) +from invokeai.backend.model_manager.configs.controlnet import ControlAdapterDefaultSettings +from invokeai.backend.model_manager.configs.identification_utils import ( + NotAMatchError, + raise_for_override_fields, + raise_if_not_dir, + raise_if_not_file, + state_dict_has_any_keys_ending_with, + state_dict_has_any_keys_starting_with, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.omi import flux_dev_1_lora, stable_diffusion_xl_1_lora +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + Flux2VariantType, + FluxLoRAFormat, + ModelFormat, + ModelType, + ZImageVariantType, +) +from invokeai.backend.model_manager.util.model_util import lora_token_vector_length +from invokeai.backend.patches.lora_conversions.anima_lora_constants import ( + has_cosmos_dit_kohya_keys, + has_cosmos_dit_peft_keys, +) +from invokeai.backend.patches.lora_conversions.flux_control_lora_utils import is_state_dict_likely_flux_control + + +class LoraModelDefaultSettings(BaseModel): + weight: float | None = Field(default=None, ge=-1, le=2, description="Default weight for this model") + model_config = ConfigDict(extra="forbid") + + +class LoRA_Config_Base(ABC, BaseModel): + """Base class for LoRA models.""" + + type: Literal[ModelType.LoRA] = Field(default=ModelType.LoRA) + trigger_phrases: set[str] | None = Field( + default=None, + description="Set of trigger phrases for this model", + ) + default_settings: LoraModelDefaultSettings | None = Field( + default=None, + description="Default settings for this model", + ) + + +def _get_flux_lora_format(mod: ModelOnDisk) -> FluxLoRAFormat | None: + # TODO(psyche): Moving this import to the function to avoid circular imports. Refactor later. + from invokeai.backend.patches.lora_conversions.formats import flux_format_from_state_dict + + state_dict = mod.load_state_dict() + value = flux_format_from_state_dict(state_dict, mod.metadata()) + return value + + +# FLUX.2 Klein context_in_dim values: 3 * Qwen3 hidden_size +# Klein 4B: 3 * 2560 = 7680, Klein 9B: 3 * 4096 = 12288 +_FLUX2_CONTEXT_IN_DIMS = {7680, 12288} + +# FLUX.2 Klein vec_in_dim values: Qwen3 hidden_size +# Klein 4B: 2560 (Qwen3-4B), Klein 9B: 4096 (Qwen3-8B) +_FLUX2_VEC_IN_DIMS = {2560, 4096} + +# FLUX.1 hidden_size is 3072. Klein 9B uses hidden_size=4096. +# Klein 4B also uses 3072, so hidden_size alone can't distinguish Klein 4B from FLUX.1. +_FLUX1_HIDDEN_SIZE = 3072 + +# FLUX.1 uses mlp_ratio=4 (ffn_dim=12288 for hidden_size=3072). +# Klein 4B uses mlp_ratio=6 (ffn_dim=18432 for hidden_size=3072). +_FLUX1_MLP_RATIO = 4 + + +def _lokr_in_dim(state_dict: dict[str | int, Any], key_prefix: str) -> int | None: + """Compute the input dimension of a LOKR layer: w1.shape[1] * w2.shape[1]. + + Supports both full LOKR (lokr_w1/lokr_w2) and factorized LOKR (lokr_w1_b/lokr_w2_b). + Returns None if the required keys are not present. + """ + if f"{key_prefix}.lokr_w1" in state_dict and f"{key_prefix}.lokr_w2" in state_dict: + return state_dict[f"{key_prefix}.lokr_w1"].shape[1] * state_dict[f"{key_prefix}.lokr_w2"].shape[1] + elif f"{key_prefix}.lokr_w1_b" in state_dict and f"{key_prefix}.lokr_w2_b" in state_dict: + return state_dict[f"{key_prefix}.lokr_w1_b"].shape[1] * state_dict[f"{key_prefix}.lokr_w2_b"].shape[1] + return None + + +def _lokr_out_dim(state_dict: dict[str | int, Any], key_prefix: str) -> int | None: + """Compute the output dimension of a LOKR layer: w1.shape[0] * w2.shape[0]. + + Supports both full LOKR (lokr_w1/lokr_w2) and factorized LOKR (lokr_w1_a/lokr_w2_a). + Returns None if the required keys are not present. + """ + if f"{key_prefix}.lokr_w1" in state_dict and f"{key_prefix}.lokr_w2" in state_dict: + return state_dict[f"{key_prefix}.lokr_w1"].shape[0] * state_dict[f"{key_prefix}.lokr_w2"].shape[0] + elif f"{key_prefix}.lokr_w1_a" in state_dict and f"{key_prefix}.lokr_w2_a" in state_dict: + return state_dict[f"{key_prefix}.lokr_w1_a"].shape[0] * state_dict[f"{key_prefix}.lokr_w2_a"].shape[0] + return None + + +def _is_flux2_lora(mod: ModelOnDisk) -> bool: + """Check if a FLUX-format LoRA is specifically for FLUX.2 (Klein) rather than FLUX.1. + + Detection is based on: + 1. Tensor shapes of embedding layers (context_embedder, vector_in) that differ between FLUX.1 and FLUX.2 + 2. Hidden size of attention layers (3072 for FLUX.1/Klein 4B, 4096 for Klein 9B) + + Returns False for ambiguous LoRAs (e.g. Klein 4B transformer-only LoRAs with no distinguishing layers). + """ + state_dict = mod.load_state_dict() + return _is_flux2_lora_state_dict(state_dict) + + +def _is_flux2_lora_state_dict(state_dict: dict[str | int, Any]) -> bool: + """Check state dict tensor shapes for FLUX.2 Klein-specific dimensions.""" + # Check diffusers/PEFT format keys (with various prefixes). + # This covers both Flux.1 diffusers naming AND Flux2 Klein diffusers naming. + for prefix in ["transformer.", "base_model.model.", ""]: + # Check context_embedder (txt_in) dimensions + # FLUX.1: context_in_dim=4096, FLUX.2 Klein 4B: 7680, Klein 9B: 12288 + ctx_key_a = f"{prefix}context_embedder.lora_A.weight" + if ctx_key_a in state_dict: + return state_dict[ctx_key_a].shape[1] in _FLUX2_CONTEXT_IN_DIMS + + # Check vector_in (time_text_embed.text_embedder) dimensions + # FLUX.1: vec_in_dim=768, FLUX.2 Klein 4B: 2560, Klein 9B: 4096 + vec_key_a = f"{prefix}time_text_embed.text_embedder.linear_1.lora_A.weight" + if vec_key_a in state_dict: + return state_dict[vec_key_a].shape[1] in _FLUX2_VEC_IN_DIMS + + # Check Flux2 Klein diffusers naming: fused QKV+MLP in single blocks. + # This key only exists in Flux2 models (Flux.1 uses separate to_q/to_k/to_v + proj_mlp). + fused_key_a = f"{prefix}single_transformer_blocks.0.attn.to_qkv_mlp_proj.lora_A.weight" + if fused_key_a in state_dict: + return True + + # Check Flux2 Klein diffusers naming: ff.linear_in (Flux.1 uses ff.net.0.proj). + ff_key_a = f"{prefix}transformer_blocks.0.ff.linear_in.lora_A.weight" + if ff_key_a in state_dict: + return True + + # Check BFL PEFT format (diffusion_model.* or base_model.model.* prefix with BFL layer names). + # Klein 9B has hidden_size=4096 (vs 3072 for FLUX.1 and Klein 4B). + # Klein 4B has same hidden_size as FLUX.1 (3072) but different mlp_ratio (6 vs 4), + # and different txt_in/vector_in dimensions. + _bfl_prefixes = ("diffusion_model.", "base_model.model.") + bfl_hidden_size: int | None = None + for key in state_dict: + if not isinstance(key, str): + continue + if not key.startswith(_bfl_prefixes): + continue + + # BFL PEFT: attention projection → check hidden_size + if key.endswith(".img_attn.proj.lora_A.weight"): + bfl_hidden_size = state_dict[key].shape[1] + if bfl_hidden_size != _FLUX1_HIDDEN_SIZE: + return True + # hidden_size=3072 is ambiguous (could be Klein 4B or FLUX.1), keep checking + + # BFL PEFT: context_embedder/txt_in + elif "txt_in" in key and key.endswith("lora_A.weight"): + return state_dict[key].shape[1] in _FLUX2_CONTEXT_IN_DIMS + + # BFL PEFT: vector_in + elif "vector_in" in key and key.endswith("lora_A.weight"): + return state_dict[key].shape[1] in _FLUX2_VEC_IN_DIMS + + # BFL LyCORIS (LoKR/LoHA): attention projection → check hidden_size via product of dims + elif key.endswith((".img_attn.proj.lokr_w1", ".img_attn.proj.lokr_w1_b")): + layer_prefix = key.rsplit(".", 1)[0] + in_dim = _lokr_in_dim(state_dict, layer_prefix) + if in_dim is not None: + if in_dim != _FLUX1_HIDDEN_SIZE: + return True + bfl_hidden_size = in_dim # ambiguous, keep checking + + # BFL LyCORIS: context_embedder/txt_in + elif "txt_in" in key and key.endswith((".lokr_w1", ".lokr_w1_b")): + layer_prefix = key.rsplit(".", 1)[0] + in_dim = _lokr_in_dim(state_dict, layer_prefix) + if in_dim is not None: + return in_dim in _FLUX2_CONTEXT_IN_DIMS + + # BFL LyCORIS: vector_in + elif "vector_in" in key and key.endswith((".lokr_w1", ".lokr_w1_b")): + layer_prefix = key.rsplit(".", 1)[0] + in_dim = _lokr_in_dim(state_dict, layer_prefix) + if in_dim is not None: + return in_dim in _FLUX2_VEC_IN_DIMS + + # BFL PEFT/LyCORIS: hidden_size matches FLUX.1. Check MLP ratio to distinguish Klein 4B. + # Klein 4B uses mlp_ratio=6 (ffn_dim=18432), FLUX.1 uses mlp_ratio=4 (ffn_dim=12288). + if bfl_hidden_size == _FLUX1_HIDDEN_SIZE: + for key in state_dict: + if not isinstance(key, str): + continue + if key.startswith(_bfl_prefixes) and key.endswith(".img_mlp.0.lora_B.weight"): + ffn_dim = state_dict[key].shape[0] + if ffn_dim != bfl_hidden_size * _FLUX1_MLP_RATIO: + return True + break + # BFL LyCORIS: check output dim of img_mlp.0 via product of dims + if key.startswith(_bfl_prefixes) and key.endswith((".img_mlp.0.lokr_w1", ".img_mlp.0.lokr_w1_a")): + layer_prefix = key.rsplit(".", 1)[0] + out_dim = _lokr_out_dim(state_dict, layer_prefix) + if out_dim is not None and out_dim != bfl_hidden_size * _FLUX1_MLP_RATIO: + return True + break + + # Check kohya format: look for context_embedder or vector_in keys + # Kohya format uses lora_unet_ prefix with underscores instead of dots + for key in state_dict: + if not isinstance(key, str): + continue + if key.startswith("lora_unet_txt_in.") or key.startswith("lora_unet_context_embedder."): + if key.endswith("lora_down.weight"): + return state_dict[key].shape[1] in _FLUX2_CONTEXT_IN_DIMS + # Kohya LyCORIS (LoKR) + elif key.endswith((".lokr_w1", ".lokr_w1_b")): + layer_prefix = key.rsplit(".", 1)[0] + in_dim = _lokr_in_dim(state_dict, layer_prefix) + if in_dim is not None: + return in_dim in _FLUX2_CONTEXT_IN_DIMS + if key.startswith("lora_unet_vector_in.") or key.startswith("lora_unet_time_text_embed_text_embedder_"): + if key.endswith("lora_down.weight"): + return state_dict[key].shape[1] in _FLUX2_VEC_IN_DIMS + # Kohya LyCORIS (LoKR) + elif key.endswith((".lokr_w1", ".lokr_w1_b")): + layer_prefix = key.rsplit(".", 1)[0] + in_dim = _lokr_in_dim(state_dict, layer_prefix) + if in_dim is not None: + return in_dim in _FLUX2_VEC_IN_DIMS + + # Kohya format: check transformer block dimensions (hidden_size and MLP ratio). + # This handles LoRAs that only target transformer blocks (no txt_in/vector_in/context_embedder). + # Klein 9B has hidden_size=4096 (vs 3072 for FLUX.1 and Klein 4B). + # Klein 4B has same hidden_size as FLUX.1 (3072) but different mlp_ratio (6 vs 4). + kohya_hidden_size: int | None = None + for key in state_dict: + if not isinstance(key, str): + continue + if not key.startswith("lora_unet_"): + continue + + # Check img_attn_proj hidden_size + if "_img_attn_proj." in key and key.endswith("lora_down.weight"): + kohya_hidden_size = state_dict[key].shape[1] + if kohya_hidden_size != _FLUX1_HIDDEN_SIZE: + return True + break + # LoKR variant + elif "_img_attn_proj." in key and key.endswith((".lokr_w1", ".lokr_w1_b")): + layer_prefix = key.rsplit(".", 1)[0] + in_dim = _lokr_in_dim(state_dict, layer_prefix) + if in_dim is not None: + if in_dim != _FLUX1_HIDDEN_SIZE: + return True + kohya_hidden_size = in_dim + break + + # Kohya format: hidden_size matches FLUX.1. Check MLP ratio to distinguish Klein 4B. + # Klein 4B uses mlp_ratio=6 (ffn_dim=18432), FLUX.1 uses mlp_ratio=4 (ffn_dim=12288). + if kohya_hidden_size == _FLUX1_HIDDEN_SIZE: + for key in state_dict: + if not isinstance(key, str): + continue + if key.startswith("lora_unet_") and "_img_mlp_0." in key and key.endswith("lora_up.weight"): + ffn_dim = state_dict[key].shape[0] + if ffn_dim != kohya_hidden_size * _FLUX1_MLP_RATIO: + return True + break + # LoKR variant + if key.startswith("lora_unet_") and "_img_mlp_0." in key and key.endswith((".lokr_w1", ".lokr_w1_a")): + layer_prefix = key.rsplit(".", 1)[0] + out_dim = _lokr_out_dim(state_dict, layer_prefix) + if out_dim is not None and out_dim != kohya_hidden_size * _FLUX1_MLP_RATIO: + return True + break + + return False + + +def _get_flux2_lora_variant(state_dict: dict[str | int, Any]) -> Flux2VariantType | None: + """Determine FLUX.2 Klein variant (4B vs 9B) from a LoRA state dict. + + Detection is based on tensor dimensions that differ between Klein 4B and Klein 9B: + - hidden_size from attention projection: 3072 = Klein 4B, 4096 = Klein 9B + - context_in_dim from context embedder: 7680 = Klein 4B, 12288 = Klein 9B + - vec_in_dim from vector embedder: 2560 = Klein 4B, 4096 = Klein 9B + + Returns None if the variant cannot be determined (e.g. LoRA only targets layers + with identical dimensions across variants). + """ + KLEIN_4B_CONTEXT_DIM = 7680 # 3 * 2560 + KLEIN_9B_CONTEXT_DIM = 12288 # 3 * 4096 + KLEIN_4B_VEC_DIM = 2560 + KLEIN_9B_VEC_DIM = 4096 + KLEIN_4B_HIDDEN_SIZE = 3072 + KLEIN_9B_HIDDEN_SIZE = 4096 + + # Check diffusers/PEFT format keys + for prefix in ["transformer.", "base_model.model.", ""]: + # Context embedder (txt_in) dimensions + ctx_key_a = f"{prefix}context_embedder.lora_A.weight" + if ctx_key_a in state_dict: + dim = state_dict[ctx_key_a].shape[1] + if dim == KLEIN_4B_CONTEXT_DIM: + return Flux2VariantType.Klein4B + if dim == KLEIN_9B_CONTEXT_DIM: + return Flux2VariantType.Klein9B + return None + + # Vector embedder dimensions + vec_key_a = f"{prefix}time_text_embed.text_embedder.linear_1.lora_A.weight" + if vec_key_a in state_dict: + dim = state_dict[vec_key_a].shape[1] + if dim == KLEIN_4B_VEC_DIM: + return Flux2VariantType.Klein4B + if dim == KLEIN_9B_VEC_DIM: + return Flux2VariantType.Klein9B + return None + + # Attention projection hidden_size (Flux.1 diffusers naming) + attn_key_a = f"{prefix}transformer_blocks.0.attn.to_out.0.lora_A.weight" + if attn_key_a in state_dict: + dim = state_dict[attn_key_a].shape[1] + if dim == KLEIN_4B_HIDDEN_SIZE: + return Flux2VariantType.Klein4B + if dim == KLEIN_9B_HIDDEN_SIZE: + return Flux2VariantType.Klein9B + return None + + # Attention projection hidden_size (Flux2 Klein diffusers naming) + attn_key_a2 = f"{prefix}transformer_blocks.0.attn.to_add_out.lora_A.weight" + if attn_key_a2 in state_dict: + dim = state_dict[attn_key_a2].shape[1] + if dim == KLEIN_4B_HIDDEN_SIZE: + return Flux2VariantType.Klein4B + if dim == KLEIN_9B_HIDDEN_SIZE: + return Flux2VariantType.Klein9B + return None + + # Fused QKV+MLP hidden_size (Flux2 Klein diffusers naming) + fused_key_a = f"{prefix}single_transformer_blocks.0.attn.to_qkv_mlp_proj.lora_A.weight" + if fused_key_a in state_dict: + dim = state_dict[fused_key_a].shape[1] + if dim == KLEIN_4B_HIDDEN_SIZE: + return Flux2VariantType.Klein4B + if dim == KLEIN_9B_HIDDEN_SIZE: + return Flux2VariantType.Klein9B + return None + + # Check BFL PEFT/LyCORIS format (diffusion_model.* or base_model.model.* prefix with BFL names) + _bfl_prefixes = ("diffusion_model.", "base_model.model.") + for key in state_dict: + if not isinstance(key, str): + continue + if not key.startswith(_bfl_prefixes): + continue + + # BFL PEFT: context embedder (txt_in) + if "txt_in" in key and key.endswith("lora_A.weight"): + dim = state_dict[key].shape[1] + if dim == KLEIN_4B_CONTEXT_DIM: + return Flux2VariantType.Klein4B + if dim == KLEIN_9B_CONTEXT_DIM: + return Flux2VariantType.Klein9B + return None + + # BFL PEFT: vector embedder (vector_in) + if "vector_in" in key and key.endswith("lora_A.weight"): + dim = state_dict[key].shape[1] + if dim == KLEIN_4B_VEC_DIM: + return Flux2VariantType.Klein4B + if dim == KLEIN_9B_VEC_DIM: + return Flux2VariantType.Klein9B + return None + + # BFL PEFT: attention projection + if key.endswith(".img_attn.proj.lora_A.weight"): + dim = state_dict[key].shape[1] + if dim == KLEIN_4B_HIDDEN_SIZE: + return Flux2VariantType.Klein4B + if dim == KLEIN_9B_HIDDEN_SIZE: + return Flux2VariantType.Klein9B + return None + + # BFL LyCORIS (LoKR): context embedder (txt_in) + if "txt_in" in key and key.endswith((".lokr_w1", ".lokr_w1_b")): + layer_prefix = key.rsplit(".", 1)[0] + in_dim = _lokr_in_dim(state_dict, layer_prefix) + if in_dim is not None: + if in_dim == KLEIN_4B_CONTEXT_DIM: + return Flux2VariantType.Klein4B + if in_dim == KLEIN_9B_CONTEXT_DIM: + return Flux2VariantType.Klein9B + return None + + # BFL LyCORIS (LoKR): vector embedder (vector_in) + if "vector_in" in key and key.endswith((".lokr_w1", ".lokr_w1_b")): + layer_prefix = key.rsplit(".", 1)[0] + in_dim = _lokr_in_dim(state_dict, layer_prefix) + if in_dim is not None: + if in_dim == KLEIN_4B_VEC_DIM: + return Flux2VariantType.Klein4B + if in_dim == KLEIN_9B_VEC_DIM: + return Flux2VariantType.Klein9B + return None + + # BFL LyCORIS (LoKR): attention projection + if key.endswith((".img_attn.proj.lokr_w1", ".img_attn.proj.lokr_w1_b")): + layer_prefix = key.rsplit(".", 1)[0] + in_dim = _lokr_in_dim(state_dict, layer_prefix) + if in_dim is not None: + if in_dim == KLEIN_4B_HIDDEN_SIZE: + return Flux2VariantType.Klein4B + if in_dim == KLEIN_9B_HIDDEN_SIZE: + return Flux2VariantType.Klein9B + return None + + # Check kohya format + for key in state_dict: + if not isinstance(key, str): + continue + if key.startswith("lora_unet_txt_in.") or key.startswith("lora_unet_context_embedder."): + if key.endswith("lora_down.weight"): + dim = state_dict[key].shape[1] + if dim == KLEIN_4B_CONTEXT_DIM: + return Flux2VariantType.Klein4B + if dim == KLEIN_9B_CONTEXT_DIM: + return Flux2VariantType.Klein9B + return None + # Kohya LyCORIS (LoKR) + elif key.endswith((".lokr_w1", ".lokr_w1_b")): + layer_prefix = key.rsplit(".", 1)[0] + in_dim = _lokr_in_dim(state_dict, layer_prefix) + if in_dim is not None: + if in_dim == KLEIN_4B_CONTEXT_DIM: + return Flux2VariantType.Klein4B + if in_dim == KLEIN_9B_CONTEXT_DIM: + return Flux2VariantType.Klein9B + return None + if key.startswith("lora_unet_vector_in.") or key.startswith("lora_unet_time_text_embed_text_embedder_"): + if key.endswith("lora_down.weight"): + dim = state_dict[key].shape[1] + if dim == KLEIN_4B_VEC_DIM: + return Flux2VariantType.Klein4B + if dim == KLEIN_9B_VEC_DIM: + return Flux2VariantType.Klein9B + return None + # Kohya LyCORIS (LoKR) + elif key.endswith((".lokr_w1", ".lokr_w1_b")): + layer_prefix = key.rsplit(".", 1)[0] + in_dim = _lokr_in_dim(state_dict, layer_prefix) + if in_dim is not None: + if in_dim == KLEIN_4B_VEC_DIM: + return Flux2VariantType.Klein4B + if in_dim == KLEIN_9B_VEC_DIM: + return Flux2VariantType.Klein9B + return None + + # Kohya format: check transformer block dimensions (hidden_size from img_attn_proj). + # This handles LoRAs that only target transformer blocks (no txt_in/vector_in/context_embedder). + for key in state_dict: + if not isinstance(key, str): + continue + if not key.startswith("lora_unet_"): + continue + + # Check img_attn_proj hidden_size + if "_img_attn_proj." in key and key.endswith("lora_down.weight"): + dim = state_dict[key].shape[1] + if dim == KLEIN_4B_HIDDEN_SIZE: + return Flux2VariantType.Klein4B + if dim == KLEIN_9B_HIDDEN_SIZE: + return Flux2VariantType.Klein9B + return None + # LoKR variant + elif "_img_attn_proj." in key and key.endswith((".lokr_w1", ".lokr_w1_b")): + layer_prefix = key.rsplit(".", 1)[0] + in_dim = _lokr_in_dim(state_dict, layer_prefix) + if in_dim is not None: + if in_dim == KLEIN_4B_HIDDEN_SIZE: + return Flux2VariantType.Klein4B + if in_dim == KLEIN_9B_HIDDEN_SIZE: + return Flux2VariantType.Klein9B + return None + + return None + + +class LoRA_OMI_Config_Base(LoRA_Config_Base): + format: Literal[ModelFormat.OMI] = Field(default=ModelFormat.OMI) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_omi_lora(mod) + + cls._validate_base(mod) + + return cls(**override_fields) + + @classmethod + def _validate_base(cls, mod: ModelOnDisk) -> None: + """Raise `NotAMatch` if the model base does not match this config class.""" + expected_base = cls.model_fields["base"].default + recognized_base = cls._get_base_or_raise(mod) + if expected_base is not recognized_base: + raise NotAMatchError(f"base is {recognized_base}, not {expected_base}") + + @classmethod + def _validate_looks_like_omi_lora(cls, mod: ModelOnDisk) -> None: + """Raise `NotAMatch` if the model metadata does not look like an OMI LoRA.""" + flux_format = _get_flux_lora_format(mod) + if flux_format in [FluxLoRAFormat.Control, FluxLoRAFormat.Diffusers]: + raise NotAMatchError("model looks like ControlLoRA or Diffusers LoRA") + + metadata = mod.metadata() + + metadata_looks_like_omi_lora = ( + bool(metadata.get("modelspec.sai_model_spec")) + and metadata.get("ot_branch") == "omi_format" + and metadata.get("modelspec.architecture", "").split("/")[1].lower() == "lora" + ) + + if not metadata_looks_like_omi_lora: + raise NotAMatchError("metadata does not look like OMI LoRA") + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk) -> Literal[BaseModelType.Flux, BaseModelType.StableDiffusionXL]: + metadata = mod.metadata() + architecture = metadata["modelspec.architecture"] + + if architecture == stable_diffusion_xl_1_lora: + return BaseModelType.StableDiffusionXL + elif architecture == flux_dev_1_lora: + return BaseModelType.Flux + else: + raise NotAMatchError(f"unrecognised/unsupported architecture for OMI LoRA: {architecture}") + + +class LoRA_OMI_SDXL_Config(LoRA_OMI_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL) + + +class LoRA_OMI_FLUX_Config(LoRA_OMI_Config_Base, Config_Base): + base: Literal[BaseModelType.Flux] = Field(default=BaseModelType.Flux) + + +class LoRA_LyCORIS_Config_Base(LoRA_Config_Base): + """Model config for LoRA/Lycoris models.""" + + type: Literal[ModelType.LoRA] = Field(default=ModelType.LoRA) + format: Literal[ModelFormat.LyCORIS] = Field(default=ModelFormat.LyCORIS) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_lora(mod) + + cls._validate_base(mod) + + return cls(**override_fields) + + @classmethod + def _validate_base(cls, mod: ModelOnDisk) -> None: + """Raise `NotAMatch` if the model base does not match this config class.""" + expected_base = cls.model_fields["base"].default + recognized_base = cls._get_base_or_raise(mod) + if expected_base is not recognized_base: + raise NotAMatchError(f"base is {recognized_base}, not {expected_base}") + + @classmethod + def _validate_looks_like_lora(cls, mod: ModelOnDisk) -> None: + # First rule out ControlLoRA + flux_format = _get_flux_lora_format(mod) + if flux_format in [FluxLoRAFormat.Control]: + raise NotAMatchError("model looks like Control LoRA") + + # If it's a recognized Flux LoRA format (Kohya, Diffusers, OneTrainer, AIToolkit, XLabs, etc.), + # it's valid and we skip the heuristic check + if flux_format is not None: + return + + # Note: Existence of these key prefixes/suffixes does not guarantee that this is a LoRA. + # Some main models have these keys, likely due to the creator merging in a LoRA. + has_key_with_lora_prefix = state_dict_has_any_keys_starting_with( + mod.load_state_dict(), + { + "lora_te_", + "lora_unet_", + "lora_te1_", + "lora_te2_", + "lora_transformer_", + }, + ) + + has_key_with_lora_suffix = state_dict_has_any_keys_ending_with( + mod.load_state_dict(), + { + "to_k_lora.up.weight", + "to_q_lora.down.weight", + "lora_A.weight", + "lora_B.weight", + # LyCORIS LoKR suffixes + "lokr_w1", + "lokr_w2", + # LyCORIS LoHA suffixes + "hada_w1_a", + "hada_w2_a", + }, + ) + + if not has_key_with_lora_prefix and not has_key_with_lora_suffix: + raise NotAMatchError("model does not match LyCORIS LoRA heuristics") + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType: + if _get_flux_lora_format(mod): + if _is_flux2_lora(mod): + return BaseModelType.Flux2 + return BaseModelType.Flux + + state_dict = mod.load_state_dict() + str_keys = [k for k in state_dict.keys() if isinstance(k, str)] + + # Rule out Anima LoRAs — their lora_te_ keys have shapes that + # lora_token_vector_length() misidentifies as SD2/SDXL. + if has_cosmos_dit_kohya_keys(str_keys) or has_cosmos_dit_peft_keys(str_keys): + raise NotAMatchError("model looks like an Anima LoRA, not a Stable Diffusion LoRA") + + # If we've gotten here, we assume that the model is a Stable Diffusion model + token_vector_length = lora_token_vector_length(state_dict) + if token_vector_length == 768: + return BaseModelType.StableDiffusion1 + elif token_vector_length == 1024: + return BaseModelType.StableDiffusion2 + elif token_vector_length == 1280: + return BaseModelType.StableDiffusionXL # recognizes format at https://civitai.com/models/224641 + elif token_vector_length == 2048: + return BaseModelType.StableDiffusionXL + else: + raise NotAMatchError(f"unrecognized token vector length {token_vector_length}") + + +class LoRA_LyCORIS_SD1_Config(LoRA_LyCORIS_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion1] = Field(default=BaseModelType.StableDiffusion1) + + +class LoRA_LyCORIS_SD2_Config(LoRA_LyCORIS_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion2] = Field(default=BaseModelType.StableDiffusion2) + + +class LoRA_LyCORIS_SDXL_Config(LoRA_LyCORIS_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL) + + +class LoRA_LyCORIS_FLUX_Config(LoRA_LyCORIS_Config_Base, Config_Base): + base: Literal[BaseModelType.Flux] = Field(default=BaseModelType.Flux) + + +class LoRA_LyCORIS_Flux2_Config(LoRA_LyCORIS_Config_Base, Config_Base): + """Model config for FLUX.2 (Klein) LoRA models in LyCORIS format.""" + + base: Literal[BaseModelType.Flux2] = Field(default=BaseModelType.Flux2) + variant: Flux2VariantType | None = Field(default=None) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + raise_for_override_fields(cls, override_fields) + cls._validate_looks_like_lora(mod) + cls._validate_base(mod) + override_fields.setdefault("variant", _get_flux2_lora_variant(mod.load_state_dict())) + return cls(**override_fields) + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType: + if _get_flux_lora_format(mod) and _is_flux2_lora(mod): + return BaseModelType.Flux2 + raise NotAMatchError("model is not a FLUX.2 LoRA") + + +class LoRA_LyCORIS_ZImage_Config(LoRA_LyCORIS_Config_Base, Config_Base): + """Model config for Z-Image LoRA models in LyCORIS format.""" + + base: Literal[BaseModelType.ZImage] = Field(default=BaseModelType.ZImage) + variant: ZImageVariantType | None = Field(default=None) + + @classmethod + def _validate_looks_like_lora(cls, mod: ModelOnDisk) -> None: + """Z-Image LoRAs have different key patterns than SD/SDXL LoRAs. + + Z-Image LoRAs use keys like: + - diffusion_model.layers.X.attention.to_k.lora_down.weight (DoRA format) + - diffusion_model.layers.X.attention.to_k.lora_A.weight (PEFT format) + - diffusion_model.layers.X.attention.to_k.dora_scale (DoRA scale) + - lora_unet__layers_X_attention_to_k.lora_down.weight (Kohya format) + """ + from invokeai.backend.patches.lora_conversions.z_image_lora_conversion_utils import ( + is_state_dict_likely_z_image_kohya_lora, + ) + + state_dict = mod.load_state_dict() + + # Check for Kohya format first + if is_state_dict_likely_z_image_kohya_lora(state_dict): + return + + # Check for Z-Image specific LoRA patterns (dot-notation formats) + has_z_image_lora_keys = state_dict_has_any_keys_starting_with( + state_dict, + { + "diffusion_model.layers.", # Z-Image S3-DiT layer pattern + "diffusion_model.context_refiner.", + "diffusion_model.noise_refiner.", + "transformer.layers.", # OneTrainer/diffusers prefix variant + "base_model.model.transformer.layers.", # PEFT-wrapped variant + }, + ) + + # Also check for LoRA weight suffixes (various formats) + has_lora_suffix = state_dict_has_any_keys_ending_with( + state_dict, + { + "lora_A.weight", + "lora_B.weight", + "lora_down.weight", + "lora_up.weight", + "dora_scale", + }, + ) + + if has_z_image_lora_keys and has_lora_suffix: + return + + raise NotAMatchError("model does not match Z-Image LoRA heuristics") + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType: + """Z-Image LoRAs are identified by their diffusion_model.layers structure. + + Z-Image uses S3-DiT architecture with layer names like: + - diffusion_model.layers.0.attention.to_k.lora_A.weight + - diffusion_model.layers.0.feed_forward.w1.lora_A.weight + - lora_unet__layers_0_attention_to_k.lora_down.weight (Kohya format) + """ + from invokeai.backend.patches.lora_conversions.z_image_lora_conversion_utils import ( + is_state_dict_likely_z_image_kohya_lora, + ) + + state_dict = mod.load_state_dict() + + # Check for Kohya format + if is_state_dict_likely_z_image_kohya_lora(state_dict): + return BaseModelType.ZImage + + # Check for Z-Image transformer layer patterns (dot-notation formats) + # Z-Image uses diffusion_model.layers.X structure (unlike Flux which uses double_blocks/single_blocks) + has_z_image_keys = state_dict_has_any_keys_starting_with( + state_dict, + { + "diffusion_model.layers.", # Z-Image S3-DiT layer pattern + "diffusion_model.context_refiner.", + "diffusion_model.noise_refiner.", + "transformer.layers.", # OneTrainer/diffusers prefix variant + "base_model.model.transformer.layers.", # PEFT-wrapped variant + }, + ) + + # If it looks like a Z-Image LoRA, return ZImage base + if has_z_image_keys: + return BaseModelType.ZImage + + raise NotAMatchError("model does not look like a Z-Image LoRA") + + +class LoRA_LyCORIS_QwenImage_Config(LoRA_LyCORIS_Config_Base, Config_Base): + """Model config for Qwen Image Edit LoRA models in LyCORIS format.""" + + base: Literal[BaseModelType.QwenImage] = Field(default=BaseModelType.QwenImage) + + @classmethod + def _validate_looks_like_lora(cls, mod: ModelOnDisk) -> None: + """Qwen Image Edit LoRAs have keys like transformer_blocks.X.attn.to_k.lora_down.weight.""" + state_dict = mod.load_state_dict() + + has_qwen_ie_keys = state_dict_has_any_keys_starting_with( + state_dict, + { + "transformer_blocks.", + "transformer.transformer_blocks.", + "lora_unet_transformer_blocks_", # Kohya format + }, + ) + has_lora_suffix = state_dict_has_any_keys_ending_with( + state_dict, + { + "lora_A.weight", + "lora_B.weight", + "lora_down.weight", + "lora_up.weight", + "dora_scale", + "lokr_w1", + "lokr_w2", # LoKR format + }, + ) + # Must NOT have diffusion_model.layers (Z-Image) or Flux-style keys. + # Flux LoRAs can have transformer.single_transformer_blocks or transformer.transformer_blocks + # (with the "transformer." prefix and "single_" variant) which would falsely match our check. + # Flux Kohya LoRAs use lora_unet_double_blocks or lora_unet_single_blocks. + has_z_image_keys = state_dict_has_any_keys_starting_with(state_dict, {"diffusion_model.layers."}) + has_flux_keys = state_dict_has_any_keys_starting_with( + state_dict, + { + "double_blocks.", + "single_blocks.", + "single_transformer_blocks.", + "transformer.single_transformer_blocks.", + "lora_unet_double_blocks_", + "lora_unet_single_blocks_", + "lora_unet_single_transformer_blocks_", + }, + ) + + if has_qwen_ie_keys and has_lora_suffix and not has_z_image_keys and not has_flux_keys: + return + + raise NotAMatchError("model does not match Qwen Image LoRA heuristics") + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType: + state_dict = mod.load_state_dict() + has_qwen_ie_keys = state_dict_has_any_keys_starting_with( + state_dict, + {"transformer_blocks.", "transformer.transformer_blocks.", "lora_unet_transformer_blocks_"}, + ) + has_z_image_keys = state_dict_has_any_keys_starting_with(state_dict, {"diffusion_model.layers."}) + has_flux_keys = state_dict_has_any_keys_starting_with( + state_dict, + { + "double_blocks.", + "single_blocks.", + "single_transformer_blocks.", + "transformer.single_transformer_blocks.", + "lora_unet_double_blocks_", + "lora_unet_single_blocks_", + "lora_unet_single_transformer_blocks_", + }, + ) + + if has_qwen_ie_keys and not has_z_image_keys and not has_flux_keys: + return BaseModelType.QwenImage + raise NotAMatchError("model does not look like a Qwen Image Edit LoRA") + + +class LoRA_LyCORIS_Anima_Config(LoRA_LyCORIS_Config_Base, Config_Base): + """Model config for Anima LoRA models in LyCORIS format.""" + + base: Literal[BaseModelType.Anima] = Field(default=BaseModelType.Anima) + + @classmethod + def _validate_looks_like_lora(cls, mod: ModelOnDisk) -> None: + """Anima LoRAs use Kohya-style keys targeting Cosmos DiT blocks. + + Anima LoRAs have keys like: + - lora_unet_blocks_0_cross_attn_k_proj.lora_down.weight (Kohya format) + - diffusion_model.blocks.0.cross_attn.k_proj.lora_A.weight (diffusers PEFT format) + - transformer.blocks.0.cross_attn.k_proj.lora_A.weight (diffusers PEFT format) + + Detection requires Cosmos DiT-specific subcomponent names (cross_attn, + self_attn, mlp, adaln_modulation) to avoid false-positives on other + architectures that also use ``blocks`` in their paths. + """ + state_dict = mod.load_state_dict() + str_keys = [k for k in state_dict.keys() if isinstance(k, str)] + + has_cosmos_keys = has_cosmos_dit_kohya_keys(str_keys) or has_cosmos_dit_peft_keys(str_keys) + + # Also check for LoRA/LoKR weight suffixes + has_lora_suffix = state_dict_has_any_keys_ending_with( + state_dict, + { + "lora_A.weight", + "lora_B.weight", + "lora_down.weight", + "lora_up.weight", + "dora_scale", + ".lokr_w1", + ".lokr_w2", + }, + ) + + if has_cosmos_keys and has_lora_suffix: + return + + raise NotAMatchError("model does not match Anima LoRA heuristics") + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType: + """Anima LoRAs target Cosmos DiT blocks (blocks.X.cross_attn, blocks.X.self_attn, etc.). + + Uses Cosmos DiT-specific subcomponent names to avoid false-positives. + """ + state_dict = mod.load_state_dict() + str_keys = [k for k in state_dict.keys() if isinstance(k, str)] + + if has_cosmos_dit_kohya_keys(str_keys) or has_cosmos_dit_peft_keys(str_keys): + return BaseModelType.Anima + + raise NotAMatchError("model does not look like an Anima LoRA") + + +class ControlAdapter_Config_Base(ABC, BaseModel): + default_settings: ControlAdapterDefaultSettings | None = Field(None) + + +class ControlLoRA_LyCORIS_FLUX_Config(ControlAdapter_Config_Base, Config_Base): + """Model config for Control LoRA models.""" + + base: Literal[BaseModelType.Flux] = Field(default=BaseModelType.Flux) + type: Literal[ModelType.ControlLoRa] = Field(default=ModelType.ControlLoRa) + format: Literal[ModelFormat.LyCORIS] = Field(default=ModelFormat.LyCORIS) + + trigger_phrases: set[str] | None = Field(None) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_control_lora(mod) + + return cls(**override_fields) + + @classmethod + def _validate_looks_like_control_lora(cls, mod: ModelOnDisk) -> None: + state_dict = mod.load_state_dict() + + if not is_state_dict_likely_flux_control(state_dict): + raise NotAMatchError("model state dict does not look like a Flux Control LoRA") + + +class LoRA_Diffusers_Config_Base(LoRA_Config_Base): + """Model config for LoRA/Diffusers models.""" + + # TODO(psyche): Needs base handling. For FLUX, the Diffusers format does not indicate a folder model; it indicates + # the weights format. FLUX Diffusers LoRAs are single files. + + format: Literal[ModelFormat.Diffusers] = Field(default=ModelFormat.Diffusers) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_base(mod) + + return cls(**override_fields) + + @classmethod + def _validate_base(cls, mod: ModelOnDisk) -> None: + """Raise `NotAMatch` if the model base does not match this config class.""" + expected_base = cls.model_fields["base"].default + recognized_base = cls._get_base_or_raise(mod) + if expected_base is not recognized_base: + raise NotAMatchError(f"base is {recognized_base}, not {expected_base}") + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType: + if _get_flux_lora_format(mod): + if _is_flux2_lora(mod): + return BaseModelType.Flux2 + return BaseModelType.Flux + + # If we've gotten here, we assume that the LoRA is a Stable Diffusion LoRA + path_to_weight_file = cls._get_weight_file_or_raise(mod) + state_dict = mod.load_state_dict(path_to_weight_file) + token_vector_length = lora_token_vector_length(state_dict) + + match token_vector_length: + case 768: + return BaseModelType.StableDiffusion1 + case 1024: + return BaseModelType.StableDiffusion2 + case 1280: + return BaseModelType.StableDiffusionXL # recognizes format at https://civitai.com/models/224641 + case 2048: + return BaseModelType.StableDiffusionXL + case _: + raise NotAMatchError(f"unrecognized token vector length {token_vector_length}") + + @classmethod + def _get_weight_file_or_raise(cls, mod: ModelOnDisk) -> Path: + suffixes = ["bin", "safetensors"] + weight_files = [mod.path / f"pytorch_lora_weights.{sfx}" for sfx in suffixes] + for wf in weight_files: + if wf.exists(): + return wf + raise NotAMatchError("missing pytorch_lora_weights.bin or pytorch_lora_weights.safetensors") + + +class LoRA_Diffusers_SD1_Config(LoRA_Diffusers_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion1] = Field(default=BaseModelType.StableDiffusion1) + + +class LoRA_Diffusers_SD2_Config(LoRA_Diffusers_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion2] = Field(default=BaseModelType.StableDiffusion2) + + +class LoRA_Diffusers_SDXL_Config(LoRA_Diffusers_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL) + + +class LoRA_Diffusers_FLUX_Config(LoRA_Diffusers_Config_Base, Config_Base): + base: Literal[BaseModelType.Flux] = Field(default=BaseModelType.Flux) + + +class LoRA_Diffusers_Flux2_Config(LoRA_Diffusers_Config_Base, Config_Base): + """Model config for FLUX.2 (Klein) LoRA models in Diffusers format.""" + + base: Literal[BaseModelType.Flux2] = Field(default=BaseModelType.Flux2) + variant: Flux2VariantType | None = Field(default=None) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + raise_for_override_fields(cls, override_fields) + cls._validate_base(mod) + path_to_weight_file = cls._get_weight_file_or_raise(mod) + state_dict = mod.load_state_dict(path_to_weight_file) + override_fields.setdefault("variant", _get_flux2_lora_variant(state_dict)) + return cls(**override_fields) + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType: + path_to_weight_file = cls._get_weight_file_or_raise(mod) + state_dict = mod.load_state_dict(path_to_weight_file) + if _is_flux2_lora_state_dict(state_dict): + return BaseModelType.Flux2 + raise NotAMatchError("model is not a FLUX.2 Diffusers LoRA") + + +class LoRA_Diffusers_ZImage_Config(LoRA_Diffusers_Config_Base, Config_Base): + """Model config for Z-Image LoRA models in Diffusers format.""" + + base: Literal[BaseModelType.ZImage] = Field(default=BaseModelType.ZImage) + variant: ZImageVariantType | None = Field(default=None) diff --git a/invokeai/backend/model_manager/configs/main.py b/invokeai/backend/model_manager/configs/main.py new file mode 100644 index 00000000000..e1e408a3483 --- /dev/null +++ b/invokeai/backend/model_manager/configs/main.py @@ -0,0 +1,1410 @@ +from abc import ABC +from typing import Any, Literal, Self + +from pydantic import BaseModel, ConfigDict, Field + +from invokeai.backend.model_manager.configs.base import ( + Checkpoint_Config_Base, + Config_Base, + Diffusers_Config_Base, + SubmodelDefinition, +) +from invokeai.backend.model_manager.configs.clip_embed import get_clip_variant_type_from_config +from invokeai.backend.model_manager.configs.identification_utils import ( + NotAMatchError, + common_config_paths, + get_config_dict_or_raise, + raise_for_class_name, + raise_for_override_fields, + raise_if_not_dir, + raise_if_not_file, + state_dict_has_any_keys_exact, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + Flux2VariantType, + FluxVariantType, + ModelFormat, + ModelType, + ModelVariantType, + QwenImageVariantType, + SchedulerPredictionType, + SubModelType, + ZImageVariantType, +) +from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor +from invokeai.backend.stable_diffusion.schedulers.schedulers import SCHEDULER_NAME_VALUES + +DEFAULTS_PRECISION = Literal["fp16", "fp32"] + + +class MainModelDefaultSettings(BaseModel): + vae: str | None = Field(default=None, description="Default VAE for this model (model key)") + vae_precision: DEFAULTS_PRECISION | None = Field(default=None, description="Default VAE precision for this model") + scheduler: SCHEDULER_NAME_VALUES | None = Field(default=None, description="Default scheduler for this model") + steps: int | None = Field(default=None, gt=0, description="Default number of steps for this model") + cfg_scale: float | None = Field(default=None, ge=1, description="Default CFG Scale for this model") + cfg_rescale_multiplier: float | None = Field( + default=None, ge=0, lt=1, description="Default CFG Rescale Multiplier for this model" + ) + width: int | None = Field(default=None, multiple_of=8, ge=64, description="Default width for this model") + height: int | None = Field(default=None, multiple_of=8, ge=64, description="Default height for this model") + guidance: float | None = Field(default=None, ge=1, description="Default Guidance for this model") + cpu_only: bool | None = Field(default=None, description="Whether this model should run on CPU only") + fp8_storage: bool | None = Field( + default=None, + description="Store weights in FP8 to reduce VRAM usage (~50% savings). Weights are cast to compute dtype during inference.", + ) + + model_config = ConfigDict(extra="forbid") + + @classmethod + def from_base( + cls, + base: BaseModelType, + variant: Flux2VariantType | FluxVariantType | ModelVariantType | ZImageVariantType | None = None, + ) -> Self | None: + match base: + case BaseModelType.StableDiffusion1: + return cls(width=512, height=512) + case BaseModelType.StableDiffusion2: + return cls(width=768, height=768) + case BaseModelType.StableDiffusionXL: + return cls(width=1024, height=1024) + case BaseModelType.ZImage: + # Different defaults based on variant + if variant == ZImageVariantType.ZBase: + # Undistilled base model needs more steps and supports CFG + # Recommended: steps=28-50, cfg_scale=3.0-5.0 + return cls(steps=50, cfg_scale=4.0, width=1024, height=1024) + else: + # Turbo (distilled) uses fewer steps, no CFG + return cls(steps=9, cfg_scale=1.0, width=1024, height=1024) + case BaseModelType.Anima: + return cls(steps=35, cfg_scale=4.5, width=1024, height=1024) + case BaseModelType.Flux2: + # Different defaults based on variant + if variant in (Flux2VariantType.Klein4BBase, Flux2VariantType.Klein9BBase): + # Undistilled base models need more steps + return cls(steps=28, cfg_scale=1.0, width=1024, height=1024) + else: + # Distilled models (Klein 4B, Klein 9B) use fewer steps + return cls(steps=4, cfg_scale=1.0, width=1024, height=1024) + case BaseModelType.QwenImage: + return cls(steps=40, cfg_scale=4.0, width=1024, height=1024) + case _: + # TODO(psyche): Do we want defaults for other base types? + return None + + +class Main_Config_Base(ABC, BaseModel): + type: Literal[ModelType.Main] = Field(default=ModelType.Main) + trigger_phrases: set[str] | None = Field( + default=None, + description="Set of trigger phrases for this model", + ) + default_settings: MainModelDefaultSettings | None = Field( + default=None, + description="Default settings for this model", + ) + + +def _has_bnb_nf4_keys(state_dict: dict[str | int, Any]) -> bool: + bnb_nf4_keys = { + "double_blocks.0.img_attn.proj.weight.quant_state.bitsandbytes__nf4", + "model.diffusion_model.double_blocks.0.img_attn.proj.weight.quant_state.bitsandbytes__nf4", + } + return any(key in state_dict for key in bnb_nf4_keys) + + +def _has_ggml_tensors(state_dict: dict[str | int, Any]) -> bool: + return any(isinstance(v, GGMLTensor) for v in state_dict.values()) + + +def _has_main_keys(state_dict: dict[str | int, Any]) -> bool: + for key in state_dict.keys(): + if isinstance(key, int): + continue + elif key.startswith( + ( + "cond_stage_model.", + "first_stage_model.", + "model.diffusion_model.", + # Some FLUX checkpoint files contain transformer keys prefixed with "model.diffusion_model". + # This prefix is typically used to distinguish between multiple models bundled in a single file. + "model.diffusion_model.double_blocks.", + ) + ): + return True + elif key.startswith("double_blocks.") and "ip_adapter" not in key: + # FLUX models in the official BFL format contain keys with the "double_blocks." prefix, but we must be + # careful to avoid false positives on XLabs FLUX IP-Adapter models. + return True + return False + + +def _has_z_image_keys(state_dict: dict[str | int, Any]) -> bool: + """Check if state dict contains Z-Image S3-DiT transformer keys. + + This function returns True only for Z-Image main models, not LoRAs. + LoRAs are excluded by checking for LoRA-specific weight suffixes. + """ + # Z-Image specific keys that distinguish it from other models + z_image_specific_keys = { + "cap_embedder", # Caption embedder - unique to Z-Image + "context_refiner", # Context refiner blocks + "cap_pad_token", # Caption padding token + } + + # LoRA-specific suffixes - if present, this is a LoRA not a main model + lora_suffixes = ( + ".lora_down.weight", + ".lora_up.weight", + ".lora_A.weight", + ".lora_B.weight", + ".dora_scale", + ".alpha", + ) + + # First pass: check if any key has LoRA suffixes - if so, this is a LoRA not a main model + for key in state_dict.keys(): + if isinstance(key, int): + continue + if key.endswith(lora_suffixes): + return False + + # Second pass: check for Z-Image specific key parts + for key in state_dict.keys(): + if isinstance(key, int): + continue + # Handle both direct keys (cap_embedder.0.weight) and + # ComfyUI-style keys (model.diffusion_model.cap_embedder.0.weight) + key_parts = key.split(".") + for part in key_parts: + if part in z_image_specific_keys: + return True + + return False + + +class Main_SD_Checkpoint_Config_Base(Checkpoint_Config_Base, Main_Config_Base): + """Model config for main checkpoint models.""" + + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + + prediction_type: SchedulerPredictionType = Field() + variant: ModelVariantType = Field() + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_main_model(mod) + + cls._validate_base(mod) + + prediction_type = override_fields.pop("prediction_type", None) or cls._get_scheduler_prediction_type_or_raise( + mod + ) + + variant = override_fields.pop("variant", None) or cls._get_variant_or_raise(mod) + + return cls(**override_fields, prediction_type=prediction_type, variant=variant) + + @classmethod + def _validate_base(cls, mod: ModelOnDisk) -> None: + """Raise `NotAMatch` if the model base does not match this config class.""" + expected_base = cls.model_fields["base"].default + recognized_base = cls._get_base_or_raise(mod) + if expected_base is not recognized_base: + raise NotAMatchError(f"base is {recognized_base}, not {expected_base}") + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType: + state_dict = mod.load_state_dict() + + key_name = "model.diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight" + if key_name in state_dict and state_dict[key_name].shape[-1] == 768: + return BaseModelType.StableDiffusion1 + if key_name in state_dict and state_dict[key_name].shape[-1] == 1024: + return BaseModelType.StableDiffusion2 + + key_name = "model.diffusion_model.input_blocks.4.1.transformer_blocks.0.attn2.to_k.weight" + if key_name in state_dict and state_dict[key_name].shape[-1] == 2048: + return BaseModelType.StableDiffusionXL + elif key_name in state_dict and state_dict[key_name].shape[-1] == 1280: + return BaseModelType.StableDiffusionXLRefiner + + raise NotAMatchError("unable to determine base type from state dict") + + @classmethod + def _get_scheduler_prediction_type_or_raise(cls, mod: ModelOnDisk) -> SchedulerPredictionType: + base = cls.model_fields["base"].default + + if base is BaseModelType.StableDiffusion2: + state_dict = mod.load_state_dict() + key_name = "model.diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight" + if key_name in state_dict and state_dict[key_name].shape[-1] == 1024: + if "global_step" in state_dict: + if state_dict["global_step"] == 220000: + return SchedulerPredictionType.Epsilon + elif state_dict["global_step"] == 110000: + return SchedulerPredictionType.VPrediction + return SchedulerPredictionType.VPrediction + else: + return SchedulerPredictionType.Epsilon + + @classmethod + def _get_variant_or_raise(cls, mod: ModelOnDisk) -> ModelVariantType: + base = cls.model_fields["base"].default + + state_dict = mod.load_state_dict() + key_name = "model.diffusion_model.input_blocks.0.0.weight" + + if key_name not in state_dict: + raise NotAMatchError("unable to determine model variant from state dict") + + in_channels = state_dict["model.diffusion_model.input_blocks.0.0.weight"].shape[1] + + match in_channels: + case 4: + return ModelVariantType.Normal + case 5: + # Only SD2 has a depth variant + assert base is BaseModelType.StableDiffusion2, f"unexpected unet in_channels 5 for base '{base}'" + return ModelVariantType.Depth + case 9: + return ModelVariantType.Inpaint + case _: + raise NotAMatchError(f"unrecognized unet in_channels {in_channels} for base '{base}'") + + @classmethod + def _validate_looks_like_main_model(cls, mod: ModelOnDisk) -> None: + has_main_model_keys = _has_main_keys(mod.load_state_dict()) + if not has_main_model_keys: + raise NotAMatchError("state dict does not look like a main model") + + +class Main_Checkpoint_SD1_Config(Main_SD_Checkpoint_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion1] = Field(default=BaseModelType.StableDiffusion1) + + +class Main_Checkpoint_SD2_Config(Main_SD_Checkpoint_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion2] = Field(default=BaseModelType.StableDiffusion2) + + +class Main_Checkpoint_SDXL_Config(Main_SD_Checkpoint_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL) + + +class Main_Checkpoint_SDXLRefiner_Config(Main_SD_Checkpoint_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusionXLRefiner] = Field(default=BaseModelType.StableDiffusionXLRefiner) + + +def _is_flux2_model(state_dict: dict[str | int, Any]) -> bool: + """Check if state dict is a FLUX.2 model by examining context_embedder dimensions. + + FLUX.2 Klein uses Qwen3 encoder with larger context dimension: + - FLUX.1: context_in_dim = 4096 (T5) + - FLUX.2 Klein 4B: context_in_dim = 7680 (3×Qwen3-4B hidden size) + - FLUX.2 Klein 8B: context_in_dim = 12288 (3×Qwen3-8B hidden size) + + Also checks for FLUX.2-specific 32-channel latent space (in_channels=128 after packing). + """ + # Check context_embedder input dimension (most reliable) + # Weight shape: [hidden_size, context_in_dim] + for key in {"context_embedder.weight", "model.diffusion_model.context_embedder.weight"}: + if key in state_dict: + weight = state_dict[key] + if hasattr(weight, "shape") and len(weight.shape) >= 2: + context_in_dim = weight.shape[1] + # FLUX.2 has context_in_dim > 4096 (Qwen3 vs T5) + if context_in_dim > 4096: + return True + + # Also check in_channels - FLUX.2 uses 128 (32 latent channels × 4 packing) + for key in {"img_in.weight", "model.diffusion_model.img_in.weight"}: + if key in state_dict: + in_channels = state_dict[key].shape[1] + # FLUX.2 uses 128 in_channels (32 latent channels × 4) + # FLUX.1 uses 64 in_channels (16 latent channels × 4) + if in_channels == 128: + return True + + return False + + +def _filename_suggests_base(name: str) -> bool: + """Check if a model name/filename suggests it is a Base (undistilled) variant. + + Klein 9B Base and Klein 9B have identical architectures and cannot be distinguished + from the state dict. We use the filename as a heuristic: filenames containing "base" + (e.g. "flux-2-klein-base-9b", "FLUX.2-klein-base-9B") indicate the undistilled model. + """ + return "base" in name.lower() + + +def _get_flux2_variant(state_dict: dict[str | int, Any]) -> Flux2VariantType | None: + """Determine FLUX.2 variant from state dict. + + Distinguishes between Klein 4B and Klein 9B based on context embedding dimension: + - Klein 4B: context_in_dim = 7680 (3 × Qwen3-4B hidden_size 2560) + - Klein 9B: context_in_dim = 12288 (3 × Qwen3-8B hidden_size 4096) + + Note: Klein 9B (distilled) and Klein 9B Base (undistilled) have identical architectures + and cannot be distinguished from the state dict alone. This function defaults to Klein9B + for all 9B models. Callers should use filename heuristics to detect Klein9BBase. + + Supports both BFL format (checkpoint) and diffusers format keys: + - BFL format: txt_in.weight (context embedder) + - Diffusers format: context_embedder.weight + """ + # Context dimensions for each variant + KLEIN_4B_CONTEXT_DIM = 7680 # 3 × 2560 + KLEIN_9B_CONTEXT_DIM = 12288 # 3 × 4096 + + # Check context_embedder to determine variant + # Support both BFL format (txt_in.weight) and diffusers format (context_embedder.weight) + context_keys = { + # Diffusers format + "context_embedder.weight", + "model.diffusion_model.context_embedder.weight", + # BFL format (used by checkpoint/GGUF models) + "txt_in.weight", + "model.diffusion_model.txt_in.weight", + } + for key in context_keys: + if key in state_dict: + weight = state_dict[key] + # Handle GGUF quantized tensors which use tensor_shape instead of shape + if hasattr(weight, "tensor_shape"): + shape = weight.tensor_shape + elif hasattr(weight, "shape"): + shape = weight.shape + else: + continue + if len(shape) >= 2: + context_in_dim = shape[1] + # Determine variant based on context dimension + if context_in_dim == KLEIN_9B_CONTEXT_DIM: + # Default to Klein9B - callers use filename heuristics to detect Klein9BBase + return Flux2VariantType.Klein9B + elif context_in_dim == KLEIN_4B_CONTEXT_DIM: + # Default to Klein4B - callers use filename heuristics to detect Klein4BBase + return Flux2VariantType.Klein4B + elif context_in_dim > 4096: + # Unknown FLUX.2 variant, default to 4B + return Flux2VariantType.Klein4B + + # Check in_channels as backup - can only confirm it's FLUX.2, not which variant + for key in {"img_in.weight", "model.diffusion_model.img_in.weight"}: + if key in state_dict: + weight = state_dict[key] + # Handle GGUF quantized tensors + if hasattr(weight, "tensor_shape"): + in_channels = weight.tensor_shape[1] + elif hasattr(weight, "shape"): + in_channels = weight.shape[1] + else: + continue + if in_channels == 128: + # It's FLUX.2 but we can't determine which Klein variant, default to 4B + return Flux2VariantType.Klein4B + + return None + + +def _get_flux_variant(state_dict: dict[str | int, Any]) -> FluxVariantType | None: + # FLUX Model variant types are distinguished by input channels and the presence of certain keys. + + # Input channels are derived from the shape of either "img_in.weight" or "model.diffusion_model.img_in.weight". + # + # Known models that use the latter key: + # - https://civitai.com/models/885098?modelVersionId=990775 + # - https://civitai.com/models/1018060?modelVersionId=1596255 + # - https://civitai.com/models/978314/ultrareal-fine-tune?modelVersionId=1413133 + # + # Input channels for known FLUX models: + # - Unquantized Dev and Schnell have in_channels=64 + # - BNB-NF4 Dev and Schnell have in_channels=1 + # - FLUX Fill has in_channels=384 + # - Unsure of quantized FLUX Fill models + # - Unsure of GGUF-quantized models + + in_channels = None + for key in {"img_in.weight", "model.diffusion_model.img_in.weight"}: + if key in state_dict: + in_channels = state_dict[key].shape[1] + break + + if in_channels is None: + # TODO(psyche): Should we have a graceful fallback here? Previously we fell back to the "normal" variant, + # but this variant is no longer used for FLUX models. If we get here, but the model is definitely a FLUX + # model, we should figure out a good fallback value. + return None + + # Because FLUX Dev and Schnell models have the same in_channels, we need to check for the presence of + # certain keys to distinguish between them. + is_flux_dev = ( + "guidance_in.out_layer.weight" in state_dict + or "model.diffusion_model.guidance_in.out_layer.weight" in state_dict + ) + + if is_flux_dev and in_channels == 384: + return FluxVariantType.DevFill + elif is_flux_dev: + return FluxVariantType.Dev + else: + # Must be a Schnell model...? + return FluxVariantType.Schnell + + +class Main_Checkpoint_FLUX_Config(Checkpoint_Config_Base, Main_Config_Base, Config_Base): + """Model config for main checkpoint models.""" + + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + base: Literal[BaseModelType.Flux] = Field(default=BaseModelType.Flux) + + variant: FluxVariantType = Field() + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_main_model(mod) + + cls._validate_is_flux(mod) + + cls._validate_does_not_look_like_bnb_quantized(mod) + + cls._validate_does_not_look_like_gguf_quantized(mod) + + variant = override_fields.pop("variant", None) or cls._get_variant_or_raise(mod) + + return cls(**override_fields, variant=variant) + + @classmethod + def _validate_is_flux(cls, mod: ModelOnDisk) -> None: + state_dict = mod.load_state_dict() + if not state_dict_has_any_keys_exact( + state_dict, + { + "double_blocks.0.img_attn.norm.key_norm.scale", + "model.diffusion_model.double_blocks.0.img_attn.norm.key_norm.scale", + }, + ): + raise NotAMatchError("state dict does not look like a FLUX checkpoint") + + # Exclude FLUX.2 models - they have their own config class + if _is_flux2_model(state_dict): + raise NotAMatchError("model is a FLUX.2 model, not FLUX.1") + + @classmethod + def _get_variant_or_raise(cls, mod: ModelOnDisk) -> FluxVariantType: + # FLUX Model variant types are distinguished by input channels and the presence of certain keys. + state_dict = mod.load_state_dict() + variant = _get_flux_variant(state_dict) + + if variant is None: + # TODO(psyche): Should we have a graceful fallback here? Previously we fell back to the "normal" variant, + # but this variant is no longer used for FLUX models. If we get here, but the model is definitely a FLUX + # model, we should figure out a good fallback value. + raise NotAMatchError("unable to determine model variant from state dict") + + return variant + + @classmethod + def _validate_looks_like_main_model(cls, mod: ModelOnDisk) -> None: + has_main_model_keys = _has_main_keys(mod.load_state_dict()) + if not has_main_model_keys: + raise NotAMatchError("state dict does not look like a main model") + + @classmethod + def _validate_does_not_look_like_bnb_quantized(cls, mod: ModelOnDisk) -> None: + has_bnb_nf4_keys = _has_bnb_nf4_keys(mod.load_state_dict()) + if has_bnb_nf4_keys: + raise NotAMatchError("state dict looks like bnb quantized nf4") + + @classmethod + def _validate_does_not_look_like_gguf_quantized(cls, mod: ModelOnDisk): + has_ggml_tensors = _has_ggml_tensors(mod.load_state_dict()) + if has_ggml_tensors: + raise NotAMatchError("state dict looks like GGUF quantized") + + +class Main_Checkpoint_Flux2_Config(Checkpoint_Config_Base, Main_Config_Base, Config_Base): + """Model config for FLUX.2 checkpoint models (e.g. Klein).""" + + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + base: Literal[BaseModelType.Flux2] = Field(default=BaseModelType.Flux2) + + variant: Flux2VariantType = Field() + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_main_model(mod) + + cls._validate_is_flux2(mod) + + cls._validate_does_not_look_like_bnb_quantized(mod) + + cls._validate_does_not_look_like_gguf_quantized(mod) + + variant = override_fields.pop("variant", None) or cls._get_variant_or_raise(mod) + + return cls(**override_fields, variant=variant) + + @classmethod + def _validate_is_flux2(cls, mod: ModelOnDisk) -> None: + """Validate that this is a FLUX.2 model, not FLUX.1.""" + state_dict = mod.load_state_dict() + if not _is_flux2_model(state_dict): + raise NotAMatchError("state dict does not look like a FLUX.2 model") + + @classmethod + def _get_variant_or_raise(cls, mod: ModelOnDisk) -> Flux2VariantType: + state_dict = mod.load_state_dict() + variant = _get_flux2_variant(state_dict) + + if variant is None: + raise NotAMatchError("unable to determine FLUX.2 model variant from state dict") + + # Base (undistilled) and distilled variants share identical architectures. + # Use filename heuristic to detect the Base variant. + if variant == Flux2VariantType.Klein9B and _filename_suggests_base(mod.name): + return Flux2VariantType.Klein9BBase + if variant == Flux2VariantType.Klein4B and _filename_suggests_base(mod.name): + return Flux2VariantType.Klein4BBase + + return variant + + @classmethod + def _validate_looks_like_main_model(cls, mod: ModelOnDisk) -> None: + has_main_model_keys = _has_main_keys(mod.load_state_dict()) + if not has_main_model_keys: + raise NotAMatchError("state dict does not look like a main model") + + @classmethod + def _validate_does_not_look_like_bnb_quantized(cls, mod: ModelOnDisk) -> None: + has_bnb_nf4_keys = _has_bnb_nf4_keys(mod.load_state_dict()) + if has_bnb_nf4_keys: + raise NotAMatchError("state dict looks like bnb quantized nf4") + + @classmethod + def _validate_does_not_look_like_gguf_quantized(cls, mod: ModelOnDisk): + has_ggml_tensors = _has_ggml_tensors(mod.load_state_dict()) + if has_ggml_tensors: + raise NotAMatchError("state dict looks like GGUF quantized") + + +class Main_BnBNF4_FLUX_Config(Checkpoint_Config_Base, Main_Config_Base, Config_Base): + """Model config for main checkpoint models.""" + + base: Literal[BaseModelType.Flux] = Field(default=BaseModelType.Flux) + format: Literal[ModelFormat.BnbQuantizednf4b] = Field(default=ModelFormat.BnbQuantizednf4b) + + variant: FluxVariantType = Field() + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_main_model(mod) + + cls._validate_model_looks_like_bnb_quantized(mod) + + variant = override_fields.pop("variant", None) or cls._get_variant_or_raise(mod) + + return cls(**override_fields, variant=variant) + + @classmethod + def _get_variant_or_raise(cls, mod: ModelOnDisk) -> FluxVariantType: + # FLUX Model variant types are distinguished by input channels and the presence of certain keys. + state_dict = mod.load_state_dict() + variant = _get_flux_variant(state_dict) + + if variant is None: + # TODO(psyche): Should we have a graceful fallback here? Previously we fell back to the "normal" variant, + # but this variant is no longer used for FLUX models. If we get here, but the model is definitely a FLUX + # model, we should figure out a good fallback value. + raise NotAMatchError("unable to determine model variant from state dict") + + return variant + + @classmethod + def _validate_looks_like_main_model(cls, mod: ModelOnDisk) -> None: + has_main_model_keys = _has_main_keys(mod.load_state_dict()) + if not has_main_model_keys: + raise NotAMatchError("state dict does not look like a main model") + + @classmethod + def _validate_model_looks_like_bnb_quantized(cls, mod: ModelOnDisk) -> None: + has_bnb_nf4_keys = _has_bnb_nf4_keys(mod.load_state_dict()) + if not has_bnb_nf4_keys: + raise NotAMatchError("state dict does not look like bnb quantized nf4") + + +class Main_GGUF_FLUX_Config(Checkpoint_Config_Base, Main_Config_Base, Config_Base): + """Model config for main checkpoint models.""" + + base: Literal[BaseModelType.Flux] = Field(default=BaseModelType.Flux) + format: Literal[ModelFormat.GGUFQuantized] = Field(default=ModelFormat.GGUFQuantized) + + variant: FluxVariantType = Field() + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_main_model(mod) + + cls._validate_looks_like_gguf_quantized(mod) + + cls._validate_is_not_flux2(mod) + + variant = override_fields.pop("variant", None) or cls._get_variant_or_raise(mod) + + return cls(**override_fields, variant=variant) + + @classmethod + def _get_variant_or_raise(cls, mod: ModelOnDisk) -> FluxVariantType: + # FLUX Model variant types are distinguished by input channels and the presence of certain keys. + state_dict = mod.load_state_dict() + variant = _get_flux_variant(state_dict) + + if variant is None: + # TODO(psyche): Should we have a graceful fallback here? Previously we fell back to the "normal" variant, + # but this variant is no longer used for FLUX models. If we get here, but the model is definitely a FLUX + # model, we should figure out a good fallback value. + raise NotAMatchError("unable to determine model variant from state dict") + + return variant + + @classmethod + def _validate_looks_like_main_model(cls, mod: ModelOnDisk) -> None: + has_main_model_keys = _has_main_keys(mod.load_state_dict()) + if not has_main_model_keys: + raise NotAMatchError("state dict does not look like a main model") + + @classmethod + def _validate_looks_like_gguf_quantized(cls, mod: ModelOnDisk) -> None: + has_ggml_tensors = _has_ggml_tensors(mod.load_state_dict()) + if not has_ggml_tensors: + raise NotAMatchError("state dict does not look like GGUF quantized") + + @classmethod + def _validate_is_not_flux2(cls, mod: ModelOnDisk) -> None: + """Validate that this is NOT a FLUX.2 model.""" + state_dict = mod.load_state_dict() + if _is_flux2_model(state_dict): + raise NotAMatchError("model is a FLUX.2 model, not FLUX.1") + + +class Main_GGUF_Flux2_Config(Checkpoint_Config_Base, Main_Config_Base, Config_Base): + """Model config for GGUF-quantized FLUX.2 checkpoint models (e.g. Klein).""" + + base: Literal[BaseModelType.Flux2] = Field(default=BaseModelType.Flux2) + format: Literal[ModelFormat.GGUFQuantized] = Field(default=ModelFormat.GGUFQuantized) + + variant: Flux2VariantType = Field() + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_main_model(mod) + + cls._validate_looks_like_gguf_quantized(mod) + + cls._validate_is_flux2(mod) + + variant = override_fields.pop("variant", None) or cls._get_variant_or_raise(mod) + + return cls(**override_fields, variant=variant) + + @classmethod + def _validate_is_flux2(cls, mod: ModelOnDisk) -> None: + """Validate that this is a FLUX.2 model, not FLUX.1.""" + state_dict = mod.load_state_dict() + if not _is_flux2_model(state_dict): + raise NotAMatchError("state dict does not look like a FLUX.2 model") + + @classmethod + def _get_variant_or_raise(cls, mod: ModelOnDisk) -> Flux2VariantType: + state_dict = mod.load_state_dict() + variant = _get_flux2_variant(state_dict) + + if variant is None: + raise NotAMatchError("unable to determine FLUX.2 model variant from state dict") + + # Base (undistilled) and distilled variants share identical architectures. + # Use filename heuristic to detect the Base variant. + if variant == Flux2VariantType.Klein9B and _filename_suggests_base(mod.name): + return Flux2VariantType.Klein9BBase + if variant == Flux2VariantType.Klein4B and _filename_suggests_base(mod.name): + return Flux2VariantType.Klein4BBase + + return variant + + @classmethod + def _validate_looks_like_main_model(cls, mod: ModelOnDisk) -> None: + has_main_model_keys = _has_main_keys(mod.load_state_dict()) + if not has_main_model_keys: + raise NotAMatchError("state dict does not look like a main model") + + @classmethod + def _validate_looks_like_gguf_quantized(cls, mod: ModelOnDisk) -> None: + has_ggml_tensors = _has_ggml_tensors(mod.load_state_dict()) + if not has_ggml_tensors: + raise NotAMatchError("state dict does not look like GGUF quantized") + + +class Main_Diffusers_FLUX_Config(Diffusers_Config_Base, Main_Config_Base, Config_Base): + """Model config for FLUX.1 models in diffusers format.""" + + base: Literal[BaseModelType.Flux] = Field(BaseModelType.Flux) + variant: FluxVariantType = Field() + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + # Check for FLUX-specific pipeline or transformer class names + raise_for_class_name( + common_config_paths(mod.path), + { + "FluxPipeline", + "FluxFillPipeline", + "FluxTransformer2DModel", + }, + ) + + variant = override_fields.pop("variant", None) or cls._get_variant_or_raise(mod) + + repo_variant = override_fields.pop("repo_variant", None) or cls._get_repo_variant_or_raise(mod) + + return cls( + **override_fields, + variant=variant, + repo_variant=repo_variant, + ) + + @classmethod + def _get_variant_or_raise(cls, mod: ModelOnDisk) -> FluxVariantType: + """Determine the FLUX variant from the transformer config. + + FLUX variants are distinguished by: + - in_channels: 64 for Dev/Schnell, 384 for DevFill + - guidance_embeds: True for Dev, False for Schnell + """ + transformer_config = get_config_dict_or_raise(mod.path / "transformer" / "config.json") + + in_channels = transformer_config.get("in_channels", 64) + guidance_embeds = transformer_config.get("guidance_embeds", False) + + # DevFill has 384 input channels + if in_channels == 384: + return FluxVariantType.DevFill + + # Dev has guidance_embeds=True, Schnell has guidance_embeds=False + if guidance_embeds: + return FluxVariantType.Dev + else: + return FluxVariantType.Schnell + + +class Main_Diffusers_Flux2_Config(Diffusers_Config_Base, Main_Config_Base, Config_Base): + """Model config for FLUX.2 models in diffusers format (e.g. FLUX.2 Klein).""" + + base: Literal[BaseModelType.Flux2] = Field(BaseModelType.Flux2) + variant: Flux2VariantType = Field() + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + # Check for FLUX.2-specific pipeline class names + raise_for_class_name( + common_config_paths(mod.path), + { + "Flux2KleinPipeline", + }, + ) + + variant = override_fields.pop("variant", None) or cls._get_variant_or_raise(mod) + + repo_variant = override_fields.pop("repo_variant", None) or cls._get_repo_variant_or_raise(mod) + + return cls( + **override_fields, + variant=variant, + repo_variant=repo_variant, + ) + + @classmethod + def _get_variant_or_raise(cls, mod: ModelOnDisk) -> Flux2VariantType: + """Determine the FLUX.2 variant from the transformer config. + + FLUX.2 Klein uses Qwen3 text encoder with larger joint_attention_dim: + - Klein 4B/4B Base: joint_attention_dim = 7680 (3×Qwen3-4B hidden size) + - Klein 9B/9B Base: joint_attention_dim = 12288 (3×Qwen3-8B hidden size) + + Distilled and Base variants share identical architectures. We use a filename heuristic to detect Base models. + """ + KLEIN_4B_CONTEXT_DIM = 7680 # 3 × 2560 + KLEIN_9B_CONTEXT_DIM = 12288 # 3 × 4096 + + transformer_config = get_config_dict_or_raise(mod.path / "transformer" / "config.json") + + joint_attention_dim = transformer_config.get("joint_attention_dim", 4096) + + # Determine variant based on joint_attention_dim + if joint_attention_dim == KLEIN_9B_CONTEXT_DIM: + if _filename_suggests_base(mod.name): + return Flux2VariantType.Klein9BBase + return Flux2VariantType.Klein9B + elif joint_attention_dim == KLEIN_4B_CONTEXT_DIM: + if _filename_suggests_base(mod.name): + return Flux2VariantType.Klein4BBase + return Flux2VariantType.Klein4B + elif joint_attention_dim > 4096: + # Unknown FLUX.2 variant, default to 4B + return Flux2VariantType.Klein4B + + # Default to 4B + return Flux2VariantType.Klein4B + + +class Main_SD_Diffusers_Config_Base(Diffusers_Config_Base, Main_Config_Base): + prediction_type: SchedulerPredictionType = Field() + variant: ModelVariantType = Field() + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + raise_for_class_name( + common_config_paths(mod.path), + { + # SD 1.x and 2.x + "StableDiffusionPipeline", + "StableDiffusionInpaintPipeline", + # SDXL + "StableDiffusionXLPipeline", + "StableDiffusionXLInpaintPipeline", + # SDXL Refiner + "StableDiffusionXLImg2ImgPipeline", + # TODO(psyche): Do we actually support LCM models? I don't see using this class anywhere in the codebase. + "LatentConsistencyModelPipeline", + }, + ) + + cls._validate_base(mod) + + variant = override_fields.pop("variant", None) or cls._get_variant_or_raise(mod) + + prediction_type = override_fields.pop("prediction_type", None) or cls._get_scheduler_prediction_type_or_raise( + mod + ) + + repo_variant = override_fields.pop("repo_variant", None) or cls._get_repo_variant_or_raise(mod) + + return cls( + **override_fields, + variant=variant, + prediction_type=prediction_type, + repo_variant=repo_variant, + ) + + @classmethod + def _validate_base(cls, mod: ModelOnDisk) -> None: + """Raise `NotAMatch` if the model base does not match this config class.""" + expected_base = cls.model_fields["base"].default + recognized_base = cls._get_base_or_raise(mod) + if expected_base is not recognized_base: + raise NotAMatchError(f"base is {recognized_base}, not {expected_base}") + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType: + # Handle pipelines with a UNet (i.e SD 1.x, SD2.x, SDXL). + unet_conf = get_config_dict_or_raise(mod.path / "unet" / "config.json") + cross_attention_dim = unet_conf.get("cross_attention_dim") + match cross_attention_dim: + case 768: + return BaseModelType.StableDiffusion1 + case 1024: + return BaseModelType.StableDiffusion2 + case 1280: + return BaseModelType.StableDiffusionXLRefiner + case 2048: + return BaseModelType.StableDiffusionXL + case _: + raise NotAMatchError(f"unrecognized cross_attention_dim {cross_attention_dim}") + + @classmethod + def _get_scheduler_prediction_type_or_raise(cls, mod: ModelOnDisk) -> SchedulerPredictionType: + scheduler_conf = get_config_dict_or_raise(mod.path / "scheduler" / "scheduler_config.json") + + # TODO(psyche): Is epsilon the right default or should we raise if it's not present? + prediction_type = scheduler_conf.get("prediction_type", "epsilon") + + match prediction_type: + case "v_prediction": + return SchedulerPredictionType.VPrediction + case "epsilon": + return SchedulerPredictionType.Epsilon + case _: + raise NotAMatchError(f"unrecognized scheduler prediction_type {prediction_type}") + + @classmethod + def _get_variant_or_raise(cls, mod: ModelOnDisk) -> ModelVariantType: + base = cls.model_fields["base"].default + unet_config = get_config_dict_or_raise(mod.path / "unet" / "config.json") + in_channels = unet_config.get("in_channels") + + match in_channels: + case 4: + return ModelVariantType.Normal + case 5: + # Only SD2 has a depth variant + assert base is BaseModelType.StableDiffusion2, f"unexpected unet in_channels 5 for base '{base}'" + return ModelVariantType.Depth + case 9: + return ModelVariantType.Inpaint + case _: + raise NotAMatchError(f"unrecognized unet in_channels {in_channels} for base '{base}'") + + +class Main_Diffusers_SD1_Config(Main_SD_Diffusers_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion1] = Field(BaseModelType.StableDiffusion1) + + +class Main_Diffusers_SD2_Config(Main_SD_Diffusers_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion2] = Field(BaseModelType.StableDiffusion2) + + +class Main_Diffusers_SDXL_Config(Main_SD_Diffusers_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusionXL] = Field(BaseModelType.StableDiffusionXL) + + +class Main_Diffusers_SDXLRefiner_Config(Main_SD_Diffusers_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusionXLRefiner] = Field(BaseModelType.StableDiffusionXLRefiner) + + +class Main_Diffusers_SD3_Config(Diffusers_Config_Base, Main_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion3] = Field(BaseModelType.StableDiffusion3) + submodels: dict[SubModelType, SubmodelDefinition] | None = Field( + description="Loadable submodels in this model", + default=None, + ) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + # This check implies the base type - no further validation needed. + raise_for_class_name( + common_config_paths(mod.path), + { + "StableDiffusion3Pipeline", + "SD3Transformer2DModel", + }, + ) + + submodels = override_fields.pop("submodels", None) or cls._get_submodels_or_raise(mod) + + repo_variant = override_fields.pop("repo_variant", None) or cls._get_repo_variant_or_raise(mod) + + return cls( + **override_fields, + submodels=submodels, + repo_variant=repo_variant, + ) + + @classmethod + def _get_submodels_or_raise(cls, mod: ModelOnDisk) -> dict[SubModelType, SubmodelDefinition]: + # Example: https://huggingface.co/stabilityai/stable-diffusion-3.5-medium/blob/main/model_index.json + config = get_config_dict_or_raise(common_config_paths(mod.path)) + + submodels: dict[SubModelType, SubmodelDefinition] = {} + + for key, value in config.items(): + # Anything that starts with an underscore is top-level metadata, not a submodel + if key.startswith("_") or not (isinstance(value, list) and len(value) == 2): + continue + # The key is something like "transformer" and is a submodel - it will be in a dir of the same name. + # The value value is something like ["diffusers", "SD3Transformer2DModel"] + _library_name, class_name = value + + match class_name: + case "CLIPTextModelWithProjection": + model_type = ModelType.CLIPEmbed + path_or_prefix = (mod.path / key).resolve().as_posix() + + # We need to read the config to determine the variant of the CLIP model. + clip_embed_config = get_config_dict_or_raise( + { + mod.path / key / "config.json", + mod.path / key / "model_index.json", + } + ) + variant = get_clip_variant_type_from_config(clip_embed_config) + submodels[SubModelType(key)] = SubmodelDefinition( + path_or_prefix=path_or_prefix, + model_type=model_type, + variant=variant, + ) + case "SD3Transformer2DModel": + model_type = ModelType.Main + path_or_prefix = (mod.path / key).resolve().as_posix() + variant = None + submodels[SubModelType(key)] = SubmodelDefinition( + path_or_prefix=path_or_prefix, + model_type=model_type, + variant=variant, + ) + case _: + pass + + return submodels + + +class Main_Diffusers_CogView4_Config(Diffusers_Config_Base, Main_Config_Base, Config_Base): + base: Literal[BaseModelType.CogView4] = Field(BaseModelType.CogView4) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + # This check implies the base type - no further validation needed. + raise_for_class_name( + common_config_paths(mod.path), + { + "CogView4Pipeline", + }, + ) + + repo_variant = override_fields.pop("repo_variant", None) or cls._get_repo_variant_or_raise(mod) + + return cls( + **override_fields, + repo_variant=repo_variant, + ) + + +def _has_anima_keys(state_dict: dict[str | int, Any]) -> bool: + """Check if state dict contains Anima model keys. + + Anima models are identified by the presence of `llm_adapter` keys + (unique to Anima - the LLM Adapter that bridges Qwen3 text encoder to the Cosmos DiT) + alongside Cosmos Predict2 DiT keys (blocks, t_embedder, x_embedder, final_layer). + + The checkpoint keys may have a `net.` prefix (e.g. `net.llm_adapter.`, `net.blocks.`) + or a `model.diffusion_model.` prefix (ComfyUI bundled checkpoint format). + """ + has_llm_adapter = False + has_cosmos_dit = False + + # LLM adapter key prefixes — support bare, `net.`, and `model.diffusion_model.` prefixes + llm_adapter_prefixes = ( + "llm_adapter.", + "net.llm_adapter.", + "model.diffusion_model.llm_adapter.", + ) + + # Cosmos DiT key prefixes — support bare, `net.`, and `model.diffusion_model.` prefixes + cosmos_prefixes = ( + "blocks.", + "t_embedder.", + "x_embedder.", + "final_layer.", + "net.blocks.", + "net.t_embedder.", + "net.x_embedder.", + "net.final_layer.", + "model.diffusion_model.blocks.", + "model.diffusion_model.t_embedder.", + "model.diffusion_model.x_embedder.", + "model.diffusion_model.final_layer.", + ) + + for key in state_dict.keys(): + if isinstance(key, int): + continue + if any(key.startswith(p) for p in llm_adapter_prefixes): + has_llm_adapter = True + if any(key.startswith(p) for p in cosmos_prefixes): + has_cosmos_dit = True + if has_llm_adapter and has_cosmos_dit: + return True + + return False + + +class Main_Diffusers_ZImage_Config(Diffusers_Config_Base, Main_Config_Base, Config_Base): + """Model config for Z-Image diffusers models (Z-Image-Turbo, Z-Image-Base).""" + + base: Literal[BaseModelType.ZImage] = Field(BaseModelType.ZImage) + variant: ZImageVariantType = Field() + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + # This check implies the base type - no further validation needed. + raise_for_class_name( + common_config_paths(mod.path), + { + "ZImagePipeline", + }, + ) + + variant = override_fields.pop("variant", None) or cls._get_variant_or_raise(mod) + + repo_variant = override_fields.pop("repo_variant", None) or cls._get_repo_variant_or_raise(mod) + + return cls( + **override_fields, + variant=variant, + repo_variant=repo_variant, + ) + + @classmethod + def _get_variant_or_raise(cls, mod: ModelOnDisk) -> ZImageVariantType: + """Determine Z-Image variant from the scheduler config. + + Z-Image variants are distinguished by the scheduler shift value: + - Turbo (distilled): shift = 3.0 + - Base (undistilled): shift = 6.0 + """ + scheduler_config = get_config_dict_or_raise(mod.path / "scheduler" / "scheduler_config.json") + + 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 + + +class Main_Checkpoint_ZImage_Config(Checkpoint_Config_Base, Main_Config_Base, Config_Base): + """Model config for Z-Image single-file checkpoint models (safetensors, etc).""" + + base: Literal[BaseModelType.ZImage] = Field(default=BaseModelType.ZImage) + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + variant: ZImageVariantType = Field() + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_z_image_model(mod) + + cls._validate_does_not_look_like_gguf_quantized(mod) + + variant = override_fields.pop("variant", None) or ZImageVariantType.Turbo + + return cls(**override_fields, variant=variant) + + @classmethod + def _validate_looks_like_z_image_model(cls, mod: ModelOnDisk) -> None: + has_z_image_keys = _has_z_image_keys(mod.load_state_dict()) + if not has_z_image_keys: + raise NotAMatchError("state dict does not look like a Z-Image model") + + @classmethod + def _validate_does_not_look_like_gguf_quantized(cls, mod: ModelOnDisk) -> None: + has_ggml_tensors = _has_ggml_tensors(mod.load_state_dict()) + if has_ggml_tensors: + raise NotAMatchError("state dict looks like GGUF quantized") + + +class Main_GGUF_ZImage_Config(Checkpoint_Config_Base, Main_Config_Base, Config_Base): + """Model config for GGUF-quantized Z-Image transformer models.""" + + base: Literal[BaseModelType.ZImage] = Field(default=BaseModelType.ZImage) + format: Literal[ModelFormat.GGUFQuantized] = Field(default=ModelFormat.GGUFQuantized) + variant: ZImageVariantType = Field() + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_z_image_model(mod) + + cls._validate_looks_like_gguf_quantized(mod) + + variant = override_fields.pop("variant", None) or ZImageVariantType.Turbo + + return cls(**override_fields, variant=variant) + + @classmethod + def _validate_looks_like_z_image_model(cls, mod: ModelOnDisk) -> None: + has_z_image_keys = _has_z_image_keys(mod.load_state_dict()) + if not has_z_image_keys: + raise NotAMatchError("state dict does not look like a Z-Image model") + + @classmethod + def _validate_looks_like_gguf_quantized(cls, mod: ModelOnDisk) -> None: + has_ggml_tensors = _has_ggml_tensors(mod.load_state_dict()) + if not has_ggml_tensors: + raise NotAMatchError("state dict does not look like GGUF quantized") + + +class Main_Diffusers_QwenImage_Config(Diffusers_Config_Base, Main_Config_Base, Config_Base): + """Model config for Qwen Image diffusers models (both txt2img and edit).""" + + base: Literal[BaseModelType.QwenImage] = Field(BaseModelType.QwenImage) + variant: QwenImageVariantType | None = Field(default=None) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + # This check implies the base type - no further validation needed. + raise_for_class_name( + common_config_paths(mod.path), + { + "QwenImagePlusPipeline", + "QwenImageEditPlusPipeline", + "QwenImagePipeline", + }, + ) + + repo_variant = override_fields.pop("repo_variant", None) or cls._get_repo_variant_or_raise(mod) + variant = override_fields.pop("variant", None) or cls._get_qwen_image_variant(mod) + + return cls( + **override_fields, + repo_variant=repo_variant, + variant=variant, + ) + + @classmethod + def _get_qwen_image_variant(cls, mod: ModelOnDisk) -> QwenImageVariantType: + """Detect whether this is an edit or txt2img model from the pipeline class name.""" + import json + + model_index = mod.path / "model_index.json" + if model_index.exists(): + with open(model_index) as f: + config = json.load(f) + class_name = config.get("_class_name", "") + if "Edit" in class_name: + return QwenImageVariantType.Edit + return QwenImageVariantType.Generate + + +def _has_qwen_image_keys(state_dict: dict[str | int, Any]) -> bool: + """Check if state dict contains Qwen Image Edit transformer keys. + + Qwen Image Edit uses 'txt_in' and 'txt_norm' instead of 'context_embedder' (FLUX). + This distinguishes it from FLUX and other architectures. + """ + has_txt_in = any(isinstance(k, str) and k.startswith("txt_in.") for k in state_dict.keys()) + has_txt_norm = any(isinstance(k, str) and k.startswith("txt_norm.") for k in state_dict.keys()) + has_img_in = any(isinstance(k, str) and k.startswith("img_in.") for k in state_dict.keys()) + # Must NOT have context_embedder (which would indicate FLUX) + has_context_embedder = any(isinstance(k, str) and "context_embedder" in k for k in state_dict.keys()) + return has_txt_in and has_txt_norm and has_img_in and not has_context_embedder + + +class Main_GGUF_QwenImage_Config(Checkpoint_Config_Base, Main_Config_Base, Config_Base): + """Model config for GGUF-quantized Qwen Image transformer models.""" + + base: Literal[BaseModelType.QwenImage] = Field(default=BaseModelType.QwenImage) + format: Literal[ModelFormat.GGUFQuantized] = Field(default=ModelFormat.GGUFQuantized) + variant: QwenImageVariantType | None = Field(default=None) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + sd = mod.load_state_dict() + + if not _has_qwen_image_keys(sd): + raise NotAMatchError("state dict does not look like a Qwen Image Edit model") + + if not _has_ggml_tensors(sd): + raise NotAMatchError("state dict does not look like GGUF quantized") + + # Infer variant from the state dict if not explicitly provided. + # The Edit variant includes an extra tensor `__index_timestep_zero__` (used by the + # `zero_cond_t` dual-modulation path in diffusers' QwenImageTransformer2DModel). + # If the marker tensor is missing, fall back to the filename heuristic since older + # or alternate GGUF converters may not emit it. + explicit_variant = override_fields.pop("variant", None) + if explicit_variant is None: + if "__index_timestep_zero__" in sd: + explicit_variant = QwenImageVariantType.Edit + else: + filename = mod.path.stem.lower() + if "edit" in filename: + explicit_variant = QwenImageVariantType.Edit + else: + explicit_variant = QwenImageVariantType.Generate + + return cls(**override_fields, variant=explicit_variant) + + +class Main_Checkpoint_Anima_Config(Checkpoint_Config_Base, Main_Config_Base, Config_Base): + """Model config for Anima single-file checkpoint models (safetensors). + + Anima is built on NVIDIA Cosmos Predict2 DiT with a custom LLM Adapter + that bridges Qwen3 0.6B text encoder outputs to the DiT. + """ + + base: Literal[BaseModelType.Anima] = Field(default=BaseModelType.Anima) + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_anima_model(mod) + + return cls(**override_fields) + + @classmethod + def _validate_looks_like_anima_model(cls, mod: ModelOnDisk) -> None: + has_anima_keys = _has_anima_keys(mod.load_state_dict()) + if not has_anima_keys: + raise NotAMatchError("state dict does not look like an Anima model") diff --git a/invokeai/backend/model_manager/configs/qwen3_encoder.py b/invokeai/backend/model_manager/configs/qwen3_encoder.py new file mode 100644 index 00000000000..308539aa354 --- /dev/null +++ b/invokeai/backend/model_manager/configs/qwen3_encoder.py @@ -0,0 +1,283 @@ +import json +from typing import Any, Literal, Optional, Self + +from pydantic import Field + +from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base, Config_Base +from invokeai.backend.model_manager.configs.identification_utils import ( + NotAMatchError, + raise_for_class_name, + raise_for_override_fields, + raise_if_not_dir, + raise_if_not_file, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType, Qwen3VariantType +from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor + + +def _has_qwen3_keys(state_dict: dict[str | int, Any]) -> bool: + """Check if state dict contains Qwen3 model keys. + + Supports both: + - PyTorch/diffusers format: model.layers.0., model.embed_tokens.weight + - GGUF/llama.cpp format: blk.0., token_embd.weight + """ + # PyTorch/diffusers format indicators + pytorch_indicators = ["model.layers.0.", "model.embed_tokens.weight"] + # GGUF/llama.cpp format indicators + gguf_indicators = ["blk.0.", "token_embd.weight"] + + for key in state_dict.keys(): + if isinstance(key, str): + # Check PyTorch format + for indicator in pytorch_indicators: + if key.startswith(indicator) or key == indicator: + return True + # Check GGUF format + for indicator in gguf_indicators: + if key.startswith(indicator) or key == indicator: + return True + return False + + +def _has_ggml_tensors(state_dict: dict[str | int, Any]) -> bool: + """Check if state dict contains GGML tensors (GGUF quantized).""" + return any(isinstance(v, GGMLTensor) for v in state_dict.values()) + + +def _get_qwen3_variant_from_state_dict(state_dict: dict[str | int, Any]) -> Optional[Qwen3VariantType]: + """Determine Qwen3 variant (0.6B, 4B, or 8B) from state dict based on hidden_size. + + The hidden_size can be determined from the embed_tokens.weight tensor shape: + - Qwen3 0.6B: hidden_size = 1024 + - Qwen3 4B: hidden_size = 2560 + - Qwen3 8B: hidden_size = 4096 + + For GGUF format, the key is 'token_embd.weight'. + For PyTorch format, the key is 'model.embed_tokens.weight'. + """ + # Hidden size thresholds + QWEN3_06B_HIDDEN_SIZE = 1024 + QWEN3_4B_HIDDEN_SIZE = 2560 + QWEN3_8B_HIDDEN_SIZE = 4096 + + # Try to find embed_tokens weight + embed_key = None + for key in state_dict.keys(): + if isinstance(key, str): + if key == "model.embed_tokens.weight" or key == "token_embd.weight": + embed_key = key + break + + if embed_key is None: + return None + + tensor = state_dict[embed_key] + + # Get hidden_size from tensor shape + # Shape is [vocab_size, hidden_size] + if isinstance(tensor, GGMLTensor): + # GGUF tensor + if hasattr(tensor, "shape") and len(tensor.shape) >= 2: + hidden_size = tensor.shape[1] + else: + return None + elif hasattr(tensor, "shape"): + # PyTorch tensor + if len(tensor.shape) >= 2: + hidden_size = tensor.shape[1] + else: + return None + else: + return None + + # Determine variant based on hidden_size + if hidden_size == QWEN3_06B_HIDDEN_SIZE: + return Qwen3VariantType.Qwen3_06B + elif hidden_size == QWEN3_4B_HIDDEN_SIZE: + return Qwen3VariantType.Qwen3_4B + elif hidden_size == QWEN3_8B_HIDDEN_SIZE: + return Qwen3VariantType.Qwen3_8B + else: + # Unknown size, default to 4B (more common) + return Qwen3VariantType.Qwen3_4B + + +class Qwen3Encoder_Checkpoint_Config(Checkpoint_Config_Base, Config_Base): + """Configuration for single-file Qwen3 Encoder models (safetensors).""" + + base: Literal[BaseModelType.Any] = Field(default=BaseModelType.Any) + type: Literal[ModelType.Qwen3Encoder] = Field(default=ModelType.Qwen3Encoder) + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + cpu_only: bool | None = Field(default=None, description="Whether this model should run on CPU only") + variant: Qwen3VariantType = Field(description="Qwen3 model size variant (4B or 8B)") + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_qwen3_model(mod) + + cls._validate_does_not_look_like_gguf_quantized(mod) + + # Determine variant from state dict + variant = cls._get_variant_or_default(mod) + + return cls(variant=variant, **override_fields) + + @classmethod + def _get_variant_or_default(cls, mod: ModelOnDisk) -> Qwen3VariantType: + """Get variant from state dict, defaulting to 4B if unknown.""" + state_dict = mod.load_state_dict() + variant = _get_qwen3_variant_from_state_dict(state_dict) + return variant if variant is not None else Qwen3VariantType.Qwen3_4B + + @classmethod + def _validate_looks_like_qwen3_model(cls, mod: ModelOnDisk) -> None: + has_qwen3_keys = _has_qwen3_keys(mod.load_state_dict()) + if not has_qwen3_keys: + raise NotAMatchError("state dict does not look like a Qwen3 model") + + @classmethod + def _validate_does_not_look_like_gguf_quantized(cls, mod: ModelOnDisk) -> None: + has_ggml = _has_ggml_tensors(mod.load_state_dict()) + if has_ggml: + raise NotAMatchError("state dict looks like GGUF quantized") + + +class Qwen3Encoder_Qwen3Encoder_Config(Config_Base): + """Configuration for Qwen3 Encoder models in a diffusers-like format. + + The model weights are expected to be in a folder called text_encoder inside the model directory, + compatible with Qwen2VLForConditionalGeneration or similar architectures used by Z-Image. + """ + + base: Literal[BaseModelType.Any] = Field(default=BaseModelType.Any) + type: Literal[ModelType.Qwen3Encoder] = Field(default=ModelType.Qwen3Encoder) + format: Literal[ModelFormat.Qwen3Encoder] = Field(default=ModelFormat.Qwen3Encoder) + cpu_only: bool | None = Field(default=None, description="Whether this model should run on CPU only") + variant: Qwen3VariantType = Field(description="Qwen3 model size variant (4B or 8B)") + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + # Exclude full pipeline models - these should be matched as main models, not just Qwen3 encoders. + # Full pipelines have model_index.json at root (diffusers format) or a transformer subfolder. + model_index_path = mod.path / "model_index.json" + transformer_path = mod.path / "transformer" + if model_index_path.exists() or transformer_path.exists(): + raise NotAMatchError( + "directory looks like a full diffusers pipeline (has model_index.json or transformer folder), " + "not a standalone Qwen3 encoder" + ) + + # Check for text_encoder config - support both: + # 1. Full model structure: model_root/text_encoder/config.json + # 2. Standalone text_encoder download: model_root/config.json (when text_encoder subfolder is downloaded separately) + config_path_nested = mod.path / "text_encoder" / "config.json" + config_path_direct = mod.path / "config.json" + + if config_path_nested.exists(): + expected_config_path = config_path_nested + elif config_path_direct.exists(): + # Standalone text_encoder downloads do not bundle tokenizer files. If we see tokenizer files at the + # root next to config.json, this is a complete causal LM (TextLLM), not a Qwen3 encoder subfolder. + tokenizer_files = ("tokenizer.json", "tokenizer.model", "tokenizer_config.json") + if any((mod.path / f).exists() for f in tokenizer_files): + raise NotAMatchError( + "directory looks like a complete causal LM (config.json and tokenizer files at root), " + "not a standalone Qwen3 encoder" + ) + expected_config_path = config_path_direct + else: + raise NotAMatchError( + f"unable to load config file(s): {{PosixPath('{config_path_nested}'): 'file does not exist'}}" + ) + + # Qwen3 uses Qwen2VLForConditionalGeneration or similar + raise_for_class_name( + expected_config_path, + { + "Qwen2VLForConditionalGeneration", + "Qwen2ForCausalLM", + "Qwen3ForCausalLM", + }, + ) + + # Determine variant from config.json hidden_size + variant = cls._get_variant_from_config(expected_config_path) + + return cls(variant=variant, **override_fields) + + @classmethod + def _get_variant_from_config(cls, config_path) -> Qwen3VariantType: + """Get variant from config.json based on hidden_size.""" + QWEN3_06B_HIDDEN_SIZE = 1024 + QWEN3_4B_HIDDEN_SIZE = 2560 + QWEN3_8B_HIDDEN_SIZE = 4096 + + try: + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + hidden_size = config.get("hidden_size") + if hidden_size == QWEN3_8B_HIDDEN_SIZE: + return Qwen3VariantType.Qwen3_8B + elif hidden_size == QWEN3_4B_HIDDEN_SIZE: + return Qwen3VariantType.Qwen3_4B + elif hidden_size == QWEN3_06B_HIDDEN_SIZE: + return Qwen3VariantType.Qwen3_06B + else: + # Default to 4B for unknown sizes + return Qwen3VariantType.Qwen3_4B + except (json.JSONDecodeError, OSError): + return Qwen3VariantType.Qwen3_4B + + +class Qwen3Encoder_GGUF_Config(Checkpoint_Config_Base, Config_Base): + """Configuration for GGUF-quantized Qwen3 Encoder models.""" + + base: Literal[BaseModelType.Any] = Field(default=BaseModelType.Any) + type: Literal[ModelType.Qwen3Encoder] = Field(default=ModelType.Qwen3Encoder) + format: Literal[ModelFormat.GGUFQuantized] = Field(default=ModelFormat.GGUFQuantized) + cpu_only: bool | None = Field(default=None, description="Whether this model should run on CPU only") + variant: Qwen3VariantType = Field(description="Qwen3 model size variant (4B or 8B)") + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_qwen3_model(mod) + + cls._validate_looks_like_gguf_quantized(mod) + + # Determine variant from state dict + variant = cls._get_variant_or_default(mod) + + return cls(variant=variant, **override_fields) + + @classmethod + def _get_variant_or_default(cls, mod: ModelOnDisk) -> Qwen3VariantType: + """Get variant from state dict, defaulting to 4B if unknown.""" + state_dict = mod.load_state_dict() + variant = _get_qwen3_variant_from_state_dict(state_dict) + return variant if variant is not None else Qwen3VariantType.Qwen3_4B + + @classmethod + def _validate_looks_like_qwen3_model(cls, mod: ModelOnDisk) -> None: + has_qwen3_keys = _has_qwen3_keys(mod.load_state_dict()) + if not has_qwen3_keys: + raise NotAMatchError("state dict does not look like a Qwen3 model") + + @classmethod + def _validate_looks_like_gguf_quantized(cls, mod: ModelOnDisk) -> None: + has_ggml = _has_ggml_tensors(mod.load_state_dict()) + if not has_ggml: + raise NotAMatchError("state dict does not look like GGUF quantized") diff --git a/invokeai/backend/model_manager/configs/qwen_vl_encoder.py b/invokeai/backend/model_manager/configs/qwen_vl_encoder.py new file mode 100644 index 00000000000..27abf935d03 --- /dev/null +++ b/invokeai/backend/model_manager/configs/qwen_vl_encoder.py @@ -0,0 +1,154 @@ +import json +from pathlib import Path +from typing import Any, Iterable, Literal, Self + +from pydantic import Field +from safetensors import safe_open + +from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base, Config_Base +from invokeai.backend.model_manager.configs.identification_utils import ( + NotAMatchError, + raise_for_override_fields, + raise_if_not_dir, + raise_if_not_file, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType + +_RECOGNIZED_TEXT_ENCODER_CLASSES = { + "Qwen2_5_VLForConditionalGeneration", + "Qwen2VLForConditionalGeneration", +} + + +def _has_qwen_vl_keys(keys: Iterable[str]) -> bool: + """A Qwen2.5-VL/Qwen2-VL checkpoint must have both LM weights and a visual + tower — that's what distinguishes it from text-only Qwen3/Qwen2 encoders.""" + has_lm = False + has_vision = False + for k in keys: + if not isinstance(k, str): + continue + if not has_lm and (k == "model.embed_tokens.weight" or k.startswith("model.layers.")): + has_lm = True + if not has_vision and (k.startswith("visual.patch_embed.") or k.startswith("visual.blocks.")): + has_vision = True + if has_lm and has_vision: + return True + return False + + +def _read_safetensors_keys(path: Path) -> list[str]: + """Read only the key index from a safetensors file without loading tensor data. + + Avoids holding multi-GB encoder weights in RAM just to classify the file. + """ + with safe_open(str(path), framework="pt", device="cpu") as f: + return list(f.keys()) + + +class QwenVLEncoder_Diffusers_Config(Config_Base): + """Configuration for standalone Qwen2.5-VL encoder models in diffusers-style folder layout. + + Expected structure: + / + text_encoder/ + config.json (with `_class_name` or `architectures` listing + `Qwen2_5_VLForConditionalGeneration`) + model.safetensors + tokenizer/ + tokenizer_config.json + ... + processor/ (optional, for vision preprocessing) + preprocessor_config.json + + This lets users avoid downloading the full ~40 GB Qwen Image diffusers pipeline + when they only need the Qwen2.5-VL encoder for use with a GGUF transformer. + """ + + base: Literal[BaseModelType.Any] = Field(default=BaseModelType.Any) + type: Literal[ModelType.QwenVLEncoder] = Field(default=ModelType.QwenVLEncoder) + format: Literal[ModelFormat.QwenVLEncoder] = Field(default=ModelFormat.QwenVLEncoder) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + # Reject anything that looks like a full pipeline (those are matched as Main models). + if (mod.path / "model_index.json").exists() or (mod.path / "transformer").exists(): + raise NotAMatchError( + "directory looks like a full diffusers pipeline (has model_index.json or transformer folder), " + "not a standalone Qwen VL encoder" + ) + + text_encoder_dir = mod.path / "text_encoder" + tokenizer_dir = mod.path / "tokenizer" + + if not text_encoder_dir.is_dir(): + raise NotAMatchError("missing text_encoder/ subfolder") + if not tokenizer_dir.is_dir(): + raise NotAMatchError("missing tokenizer/ subfolder") + + config_path = text_encoder_dir / "config.json" + if not config_path.is_file(): + raise NotAMatchError(f"missing {config_path}") + + try: + with open(config_path, "r", encoding="utf-8") as f: + cfg = json.load(f) + except (OSError, json.JSONDecodeError) as e: + raise NotAMatchError(f"could not read text_encoder/config.json: {e}") from e + + class_name = cfg.get("_class_name") + architectures = cfg.get("architectures") or [] + candidates = {class_name, *architectures} - {None} + + if not candidates & _RECOGNIZED_TEXT_ENCODER_CLASSES: + raise NotAMatchError( + f"text_encoder class is {sorted(candidates) or 'unknown'}, " + f"expected one of {sorted(_RECOGNIZED_TEXT_ENCODER_CLASSES)}" + ) + + return cls(**override_fields) + + +class QwenVLEncoder_Checkpoint_Config(Checkpoint_Config_Base, Config_Base): + """Configuration for single-file Qwen2.5-VL encoder checkpoints (safetensors). + + This matches ComfyUI-style consolidated single-file encoders such as + `qwen_2.5_vl_7b_fp8_scaled.safetensors`, which bundle the language model + and the visual tower into one file (typically with FP8 + per-tensor + `weight_scale` ComfyUI quantization). + + The matching tokenizer + processor are pulled from HuggingFace + (`Qwen/Qwen2.5-VL-7B-Instruct`) on first use and cached for offline use. + """ + + base: Literal[BaseModelType.Any] = Field(default=BaseModelType.Any) + type: Literal[ModelType.QwenVLEncoder] = Field(default=ModelType.QwenVLEncoder) + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + # Only safetensors checkpoints are supported as single-file Qwen VL encoders. + # Reject other extensions cheaply before attempting to read keys. + if mod.path.suffix != ".safetensors": + raise NotAMatchError(f"expected a .safetensors file, got {mod.path.suffix or '(no suffix)'}") + + # Read only the key index — a 7GB fp8 encoder weighs ~7GB on disk, but we + # only need the key names to classify it, not the tensor data. + try: + keys = _read_safetensors_keys(mod.path) + except Exception as e: + raise NotAMatchError(f"could not read safetensors header: {e}") from e + + if not _has_qwen_vl_keys(keys): + raise NotAMatchError("state dict does not look like a Qwen2.5-VL/Qwen2-VL checkpoint") + + return cls(**override_fields) diff --git a/invokeai/backend/model_manager/configs/siglip.py b/invokeai/backend/model_manager/configs/siglip.py new file mode 100644 index 00000000000..328aae1737b --- /dev/null +++ b/invokeai/backend/model_manager/configs/siglip.py @@ -0,0 +1,45 @@ +from typing import ( + Literal, + Self, +) + +from pydantic import Field +from typing_extensions import Any + +from invokeai.backend.model_manager.configs.base import Config_Base, Diffusers_Config_Base +from invokeai.backend.model_manager.configs.identification_utils import ( + common_config_paths, + raise_for_class_name, + raise_for_override_fields, + raise_if_not_dir, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + ModelFormat, + ModelType, +) + + +class SigLIP_Diffusers_Config(Diffusers_Config_Base, Config_Base): + """Model config for SigLIP.""" + + type: Literal[ModelType.SigLIP] = Field(default=ModelType.SigLIP) + format: Literal[ModelFormat.Diffusers] = Field(default=ModelFormat.Diffusers) + base: Literal[BaseModelType.Any] = Field(default=BaseModelType.Any) + cpu_only: bool | None = Field(default=None, description="Whether this model should run on CPU only") + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + raise_for_class_name( + common_config_paths(mod.path), + { + "SiglipModel", + }, + ) + + return cls(**override_fields) diff --git a/invokeai/backend/model_manager/configs/spandrel.py b/invokeai/backend/model_manager/configs/spandrel.py new file mode 100644 index 00000000000..8ca8ad5f603 --- /dev/null +++ b/invokeai/backend/model_manager/configs/spandrel.py @@ -0,0 +1,54 @@ +from typing import ( + Literal, + Self, +) + +from pydantic import Field +from typing_extensions import Any + +from invokeai.backend.model_manager.configs.base import Config_Base +from invokeai.backend.model_manager.configs.identification_utils import ( + NotAMatchError, + raise_for_override_fields, + raise_if_not_file, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + ModelFormat, + ModelType, +) +from invokeai.backend.spandrel_image_to_image_model import SpandrelImageToImageModel + + +class Spandrel_Checkpoint_Config(Config_Base): + """Model config for Spandrel Image to Image models.""" + + base: Literal[BaseModelType.Any] = Field(default=BaseModelType.Any) + type: Literal[ModelType.SpandrelImageToImage] = Field(default=ModelType.SpandrelImageToImage) + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_spandrel_loads_model(mod) + + return cls(**override_fields) + + @classmethod + def _validate_spandrel_loads_model(cls, mod: ModelOnDisk) -> None: + try: + # It would be nice to avoid having to load the Spandrel model from disk here. A couple of options were + # explored to avoid this: + # 1. Call `SpandrelImageToImageModel.load_from_state_dict(ckpt)`, where `ckpt` is a state_dict on the meta + # device. Unfortunately, some Spandrel models perform operations during initialization that are not + # supported on meta tensors. + # 2. Spandrel has internal logic to determine a model's type from its state_dict before loading the model. + # This logic is not exposed in spandrel's public API. We could copy the logic here, but then we have to + # maintain it, and the risk of false positive detections is higher. + SpandrelImageToImageModel.load_from_file(mod.path) + except Exception as e: + raise NotAMatchError("model does not match SpandrelImageToImage heuristics") from e diff --git a/invokeai/backend/model_manager/configs/t2i_adapter.py b/invokeai/backend/model_manager/configs/t2i_adapter.py new file mode 100644 index 00000000000..a1da40e9b4b --- /dev/null +++ b/invokeai/backend/model_manager/configs/t2i_adapter.py @@ -0,0 +1,79 @@ +from typing import ( + Literal, + Self, +) + +from pydantic import Field +from typing_extensions import Any + +from invokeai.backend.model_manager.configs.base import Config_Base, Diffusers_Config_Base +from invokeai.backend.model_manager.configs.controlnet import ControlAdapterDefaultSettings +from invokeai.backend.model_manager.configs.identification_utils import ( + NotAMatchError, + common_config_paths, + get_config_dict_or_raise, + raise_for_class_name, + raise_for_override_fields, + raise_if_not_dir, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + ModelFormat, + ModelType, +) + + +class T2IAdapter_Diffusers_Config_Base(Diffusers_Config_Base): + """Model config for T2I.""" + + type: Literal[ModelType.T2IAdapter] = Field(default=ModelType.T2IAdapter) + format: Literal[ModelFormat.Diffusers] = Field(default=ModelFormat.Diffusers) + default_settings: ControlAdapterDefaultSettings | None = Field(None) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + raise_for_class_name( + common_config_paths(mod.path), + { + "T2IAdapter", + }, + ) + + cls._validate_base(mod) + + return cls(**override_fields) + + @classmethod + def _validate_base(cls, mod: ModelOnDisk) -> None: + """Raise `NotAMatch` if the model base does not match this config class.""" + expected_base = cls.model_fields["base"].default + recognized_base = cls._get_base_or_raise(mod) + if expected_base is not recognized_base: + raise NotAMatchError(f"base is {recognized_base}, not {expected_base}") + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType: + config_dict = get_config_dict_or_raise(common_config_paths(mod.path)) + + adapter_type = config_dict.get("adapter_type") + + match adapter_type: + case "full_adapter_xl": + return BaseModelType.StableDiffusionXL + case "full_adapter" | "light_adapter": + return BaseModelType.StableDiffusion1 + case _: + raise NotAMatchError(f"unrecognized adapter_type '{adapter_type}'") + + +class T2IAdapter_Diffusers_SD1_Config(T2IAdapter_Diffusers_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion1] = Field(default=BaseModelType.StableDiffusion1) + + +class T2IAdapter_Diffusers_SDXL_Config(T2IAdapter_Diffusers_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL) diff --git a/invokeai/backend/model_manager/configs/t5_encoder.py b/invokeai/backend/model_manager/configs/t5_encoder.py new file mode 100644 index 00000000000..2da417b10a5 --- /dev/null +++ b/invokeai/backend/model_manager/configs/t5_encoder.py @@ -0,0 +1,82 @@ +from typing import Any, Literal, Self + +from pydantic import Field + +from invokeai.backend.model_manager.configs.base import Config_Base +from invokeai.backend.model_manager.configs.identification_utils import ( + NotAMatchError, + raise_for_class_name, + raise_for_override_fields, + raise_if_not_dir, + state_dict_has_any_keys_ending_with, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType + + +class T5Encoder_T5Encoder_Config(Config_Base): + """Configuration for T5 Encoder models in a bespoke, diffusers-like format. The model weights are expected to be in + a folder called text_encoder_2 inside the model directory, with a config file named model.safetensors.index.json.""" + + base: Literal[BaseModelType.Any] = Field(default=BaseModelType.Any) + type: Literal[ModelType.T5Encoder] = Field(default=ModelType.T5Encoder) + format: Literal[ModelFormat.T5Encoder] = Field(default=ModelFormat.T5Encoder) + cpu_only: bool | None = Field(default=None, description="Whether this model should run on CPU only") + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + expected_config_path = mod.path / "text_encoder_2" / "config.json" + expected_class_name = "T5EncoderModel" + raise_for_class_name(expected_config_path, expected_class_name) + + cls.raise_if_doesnt_have_unquantized_config_file(mod) + + return cls(**override_fields) + + @classmethod + def raise_if_doesnt_have_unquantized_config_file(cls, mod: ModelOnDisk) -> None: + has_unquantized_config = (mod.path / "text_encoder_2" / "model.safetensors.index.json").exists() + + if not has_unquantized_config: + raise NotAMatchError("missing text_encoder_2/model.safetensors.index.json") + + +class T5Encoder_BnBLLMint8_Config(Config_Base): + """Configuration for T5 Encoder models quantized by bitsandbytes' LLM.int8.""" + + base: Literal[BaseModelType.Any] = Field(default=BaseModelType.Any) + type: Literal[ModelType.T5Encoder] = Field(default=ModelType.T5Encoder) + format: Literal[ModelFormat.BnbQuantizedLlmInt8b] = Field(default=ModelFormat.BnbQuantizedLlmInt8b) + cpu_only: bool | None = Field(default=None, description="Whether this model should run on CPU only") + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + expected_config_path = mod.path / "text_encoder_2" / "config.json" + expected_class_name = "T5EncoderModel" + raise_for_class_name(expected_config_path, expected_class_name) + + cls.raise_if_filename_doesnt_look_like_bnb_quantized(mod) + + cls.raise_if_state_dict_doesnt_look_like_bnb_quantized(mod) + + return cls(**override_fields) + + @classmethod + def raise_if_filename_doesnt_look_like_bnb_quantized(cls, mod: ModelOnDisk) -> None: + filename_looks_like_bnb = any(x for x in mod.weight_files() if "llm_int8" in x.as_posix()) + if not filename_looks_like_bnb: + raise NotAMatchError("filename does not look like bnb quantized llm_int8") + + @classmethod + def raise_if_state_dict_doesnt_look_like_bnb_quantized(cls, mod: ModelOnDisk) -> None: + has_scb_key_suffix = state_dict_has_any_keys_ending_with(mod.load_state_dict(), "SCB") + if not has_scb_key_suffix: + raise NotAMatchError("state dict does not look like bnb quantized llm_int8") diff --git a/invokeai/backend/model_manager/configs/text_llm.py b/invokeai/backend/model_manager/configs/text_llm.py new file mode 100644 index 00000000000..a0fb3e009f9 --- /dev/null +++ b/invokeai/backend/model_manager/configs/text_llm.py @@ -0,0 +1,52 @@ +from typing import ( + Literal, + Self, +) + +from pydantic import Field +from typing_extensions import Any + +from invokeai.backend.model_manager.configs.base import Config_Base, Diffusers_Config_Base +from invokeai.backend.model_manager.configs.identification_utils import ( + NotAMatchError, + common_config_paths, + get_class_name_from_config_dict_or_raise, + raise_for_override_fields, + raise_if_not_dir, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + ModelType, +) + + +class TextLLM_Diffusers_Config(Diffusers_Config_Base, Config_Base): + """Model config for text-only causal language models (e.g. Llama, Phi, Qwen, Mistral).""" + + type: Literal[ModelType.TextLLM] = Field(default=ModelType.TextLLM) + base: Literal[BaseModelType.Any] = Field(default=BaseModelType.Any) + cpu_only: bool | None = Field(default=None, description="Whether this model should run on CPU only") + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + # Check that the model's architecture is a causal language model. + # This covers LlamaForCausalLM, PhiForCausalLM, Phi3ForCausalLM, Qwen2ForCausalLM, + # MistralForCausalLM, GemmaForCausalLM, GPTNeoXForCausalLM, etc. + class_name = get_class_name_from_config_dict_or_raise(common_config_paths(mod.path)) + if not class_name.endswith("ForCausalLM"): + raise NotAMatchError(f"model architecture '{class_name}' is not a causal language model") + + # Verify tokenizer files exist to avoid runtime failures + tokenizer_files = {"tokenizer.json", "tokenizer.model", "tokenizer_config.json"} + if not any((mod.path / f).exists() for f in tokenizer_files): + raise NotAMatchError( + f"no tokenizer files found in '{mod.path}' " + f"(expected at least one of: {', '.join(sorted(tokenizer_files))})" + ) + + return cls(**override_fields) diff --git a/invokeai/backend/model_manager/configs/textual_inversion.py b/invokeai/backend/model_manager/configs/textual_inversion.py new file mode 100644 index 00000000000..c827f5234d5 --- /dev/null +++ b/invokeai/backend/model_manager/configs/textual_inversion.py @@ -0,0 +1,156 @@ +from abc import ABC +from pathlib import Path +from typing import ( + Literal, + Self, +) + +import torch +from pydantic import BaseModel, Field +from typing_extensions import Any + +from invokeai.backend.model_manager.configs.base import Config_Base +from invokeai.backend.model_manager.configs.identification_utils import ( + NotAMatchError, + raise_for_override_fields, + raise_if_not_dir, + raise_if_not_file, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + ModelFormat, + ModelType, +) + + +class TI_Config_Base(ABC, BaseModel): + type: Literal[ModelType.TextualInversion] = Field(default=ModelType.TextualInversion) + + @classmethod + def _validate_base(cls, mod: ModelOnDisk, path: Path | None = None) -> None: + expected_base = cls.model_fields["base"].default + recognized_base = cls._get_base_or_raise(mod, path) + if expected_base is not recognized_base: + raise NotAMatchError(f"base is {recognized_base}, not {expected_base}") + + @classmethod + def _file_looks_like_embedding(cls, mod: ModelOnDisk, path: Path | None = None) -> bool: + try: + p = path or mod.path + + if not p.exists(): + return False + + if p.is_dir(): + return False + + if p.name in [f"learned_embeds.{s}" for s in mod.weight_files()]: + return True + + state_dict = mod.load_state_dict(p) + + # Heuristic: textual inversion embeddings have these keys + if any(key in {"string_to_param", "emb_params", "clip_g"} for key in state_dict.keys()): + return True + + # Heuristic: small state dict with all tensor values + if (len(state_dict)) < 10 and all(isinstance(v, torch.Tensor) for v in state_dict.values()): + return True + + return False + except Exception: + return False + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk, path: Path | None = None) -> BaseModelType: + p = path or mod.path + + try: + state_dict = mod.load_state_dict(p) + except Exception as e: + raise NotAMatchError(f"unable to load state dict from {p}: {e}") from e + + try: + if "string_to_token" in state_dict: + token_dim = list(state_dict["string_to_param"].values())[0].shape[-1] + elif "emb_params" in state_dict: + token_dim = state_dict["emb_params"].shape[-1] + elif "clip_g" in state_dict: + token_dim = state_dict["clip_g"].shape[-1] + else: + token_dim = list(state_dict.values())[0].shape[0] + except Exception as e: + raise NotAMatchError(f"unable to determine token dimension from state dict in {p}: {e}") from e + + match token_dim: + case 768: + return BaseModelType.StableDiffusion1 + case 1024: + return BaseModelType.StableDiffusion2 + case 1280: + return BaseModelType.StableDiffusionXL + case _: + raise NotAMatchError(f"unrecognized token dimension {token_dim}") + + +class TI_File_Config_Base(TI_Config_Base): + """Model config for textual inversion embeddings.""" + + format: Literal[ModelFormat.EmbeddingFile] = Field(default=ModelFormat.EmbeddingFile) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + if not cls._file_looks_like_embedding(mod): + raise NotAMatchError("model does not look like a textual inversion embedding file") + + cls._validate_base(mod) + + return cls(**override_fields) + + +class TI_File_SD1_Config(TI_File_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion1] = Field(default=BaseModelType.StableDiffusion1) + + +class TI_File_SD2_Config(TI_File_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion2] = Field(default=BaseModelType.StableDiffusion2) + + +class TI_File_SDXL_Config(TI_File_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL) + + +class TI_Folder_Config_Base(TI_Config_Base): + """Model config for textual inversion embeddings.""" + + format: Literal[ModelFormat.EmbeddingFolder] = Field(default=ModelFormat.EmbeddingFolder) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + for p in mod.weight_files(): + if cls._file_looks_like_embedding(mod, p): + cls._validate_base(mod, p) + return cls(**override_fields) + + raise NotAMatchError("model does not look like a textual inversion embedding folder") + + +class TI_Folder_SD1_Config(TI_Folder_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion1] = Field(default=BaseModelType.StableDiffusion1) + + +class TI_Folder_SD2_Config(TI_Folder_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion2] = Field(default=BaseModelType.StableDiffusion2) + + +class TI_Folder_SDXL_Config(TI_Folder_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL) diff --git a/invokeai/backend/model_manager/configs/unknown.py b/invokeai/backend/model_manager/configs/unknown.py new file mode 100644 index 00000000000..13fbee1c928 --- /dev/null +++ b/invokeai/backend/model_manager/configs/unknown.py @@ -0,0 +1,44 @@ +from copy import deepcopy +from typing import Any, Literal, Self + +from pydantic import Field + +from invokeai.app.services.config.config_default import get_config +from invokeai.backend.model_manager.configs.base import Config_Base +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + ModelFormat, + ModelType, +) + +app_config = get_config() + + +class Unknown_Config(Config_Base): + """Model config for unknown models, used as a fallback when we cannot positively identify a model.""" + + base: Literal[BaseModelType.Unknown] = Field(default=BaseModelType.Unknown) + type: Literal[ModelType.Unknown] = Field(default=ModelType.Unknown) + format: Literal[ModelFormat.Unknown] = Field(default=ModelFormat.Unknown) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + """Create an Unknown_Config for models that couldn't be positively identified. + + Note: Basic path validation (file extensions, directory structure) is already + performed by ModelConfigFactory before this method is called. + """ + + cloned_override_fields = deepcopy(override_fields) + cloned_override_fields.pop("base", None) + cloned_override_fields.pop("type", None) + cloned_override_fields.pop("format", None) + + return cls( + **cloned_override_fields, + # Override the type/format/base to ensure it's marked as unknown. + base=BaseModelType.Unknown, + type=ModelType.Unknown, + format=ModelFormat.Unknown, + ) diff --git a/invokeai/backend/model_manager/configs/vae.py b/invokeai/backend/model_manager/configs/vae.py new file mode 100644 index 00000000000..5a88cf12781 --- /dev/null +++ b/invokeai/backend/model_manager/configs/vae.py @@ -0,0 +1,345 @@ +import re +from typing import ( + Literal, + Self, +) + +from pydantic import Field +from typing_extensions import Any + +from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base, Config_Base, Diffusers_Config_Base +from invokeai.backend.model_manager.configs.identification_utils import ( + NotAMatchError, + common_config_paths, + get_config_dict_or_raise, + raise_for_class_name, + raise_for_override_fields, + raise_if_not_dir, + raise_if_not_file, + state_dict_has_any_keys_starting_with, +) +from invokeai.backend.model_manager.model_on_disk import ModelOnDisk +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + ModelFormat, + ModelType, +) + +REGEX_TO_BASE: dict[str, BaseModelType] = { + r"xl": BaseModelType.StableDiffusionXL, + r"sd2": BaseModelType.StableDiffusion2, + r"vae": BaseModelType.StableDiffusion1, + r"FLUX.1-schnell_ae": BaseModelType.Flux, +} + + +def _is_qwen_image_vae(state_dict: dict[str | int, Any]) -> bool: + """Check if state dict is a Qwen Image VAE (AutoencoderKLQwenImage). + + Qwen Image VAE can be identified by: + 1. Diffusers-format encoder/decoder keys (`encoder.conv_in`, `decoder.conv_in`) + 2. 5-dimensional convolution weights (3D causal convolutions vs. standard 2D conv in SD/SDXL/FLUX VAEs) + 3. 16-dimensional latent space (z_dim=16) + """ + decoder_conv_in_key = "decoder.conv_in.weight" + if decoder_conv_in_key not in state_dict: + return False + weight = state_dict[decoder_conv_in_key] + shape = getattr(weight, "shape", None) + if shape is None or len(shape) != 5: + return False + # z_dim is the input channel dim of decoder.conv_in + return shape[1] == 16 + + +def _is_flux2_vae(state_dict: dict[str | int, Any]) -> bool: + """Check if state dict is a FLUX.2 VAE (AutoencoderKLFlux2). + + FLUX.2 VAE can be identified by: + 1. Batch Normalization layers (bn.running_mean, bn.running_var) - unique to FLUX.2 + 2. 32-dimensional latent space (decoder.conv_in has 32 input channels) + + FLUX.1 VAE has 16-dimensional latent space and no BatchNorm layers. + """ + # Check for BN layer which is unique to FLUX.2 VAE + has_bn = "bn.running_mean" in state_dict or "bn.running_var" in state_dict + + # Check for 32-channel latent space (FLUX.2 has 32, FLUX.1 has 16) + decoder_conv_in_key = "decoder.conv_in.weight" + has_32_latent_channels = decoder_conv_in_key in state_dict and state_dict[decoder_conv_in_key].shape[1] == 32 + + return has_bn or has_32_latent_channels + + +class VAE_Checkpoint_Config_Base(Checkpoint_Config_Base): + """Model config for standalone VAE models.""" + + type: Literal[ModelType.VAE] = Field(default=ModelType.VAE) + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_vae(mod) + + cls._validate_base(mod) + + return cls(**override_fields) + + @classmethod + def _validate_base(cls, mod: ModelOnDisk) -> None: + """Raise `NotAMatch` if the model base does not match this config class.""" + expected_base = cls.model_fields["base"].default + recognized_base = cls._get_base_or_raise(mod) + if expected_base is not recognized_base: + raise NotAMatchError(f"base is {recognized_base}, not {expected_base}") + + @classmethod + def _validate_looks_like_vae(cls, mod: ModelOnDisk) -> None: + state_dict = mod.load_state_dict() + if not state_dict_has_any_keys_starting_with( + state_dict, + { + "encoder.conv_in", + "decoder.conv_in", + }, + ): + raise NotAMatchError("model does not match Checkpoint VAE heuristics") + + # Exclude FLUX.2 VAEs - they have their own config class + if _is_flux2_vae(state_dict): + raise NotAMatchError("model is a FLUX.2 VAE, not a standard VAE") + + # Exclude Qwen Image VAEs - they have their own config class + if _is_qwen_image_vae(state_dict): + raise NotAMatchError("model is a Qwen Image VAE, not a standard VAE") + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType: + # First, try to identify by latent space dimensions (most reliable) + state_dict = mod.load_state_dict() + decoder_conv_in_key = "decoder.conv_in.weight" + if decoder_conv_in_key in state_dict: + latent_channels = state_dict[decoder_conv_in_key].shape[1] + if latent_channels == 16: + # Flux1 VAE has 16-dimensional latent space + return BaseModelType.Flux + elif latent_channels == 4: + # SD/SDXL VAE has 4-dimensional latent space + # Try to distinguish SD1/SD2/SDXL by name, fallback to SD1 + for regexp, base in REGEX_TO_BASE.items(): + if re.search(regexp, mod.path.name, re.IGNORECASE): + return base + # Default to SD1 if we can't determine from name + return BaseModelType.StableDiffusion1 + + # Fallback: guess based on name + for regexp, base in REGEX_TO_BASE.items(): + if re.search(regexp, mod.path.name, re.IGNORECASE): + return base + + raise NotAMatchError("cannot determine base type") + + +class VAE_Checkpoint_SD1_Config(VAE_Checkpoint_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion1] = Field(default=BaseModelType.StableDiffusion1) + + +class VAE_Checkpoint_SD2_Config(VAE_Checkpoint_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion2] = Field(default=BaseModelType.StableDiffusion2) + + +class VAE_Checkpoint_SDXL_Config(VAE_Checkpoint_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL) + + +class VAE_Checkpoint_FLUX_Config(VAE_Checkpoint_Config_Base, Config_Base): + base: Literal[BaseModelType.Flux] = Field(default=BaseModelType.Flux) + + +class VAE_Checkpoint_Flux2_Config(Checkpoint_Config_Base, Config_Base): + """Model config for FLUX.2 VAE checkpoint models (AutoencoderKLFlux2).""" + + type: Literal[ModelType.VAE] = Field(default=ModelType.VAE) + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + base: Literal[BaseModelType.Flux2] = Field(default=BaseModelType.Flux2) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + cls._validate_looks_like_vae(mod) + + cls._validate_is_flux2_vae(mod) + + return cls(**override_fields) + + @classmethod + def _validate_looks_like_vae(cls, mod: ModelOnDisk) -> None: + if not state_dict_has_any_keys_starting_with( + mod.load_state_dict(), + { + "encoder.conv_in", + "decoder.conv_in", + }, + ): + raise NotAMatchError("model does not match Checkpoint VAE heuristics") + + @classmethod + def _validate_is_flux2_vae(cls, mod: ModelOnDisk) -> None: + """Validate that this is a FLUX.2 VAE, not FLUX.1.""" + state_dict = mod.load_state_dict() + if not _is_flux2_vae(state_dict): + raise NotAMatchError("state dict does not look like a FLUX.2 VAE") + + +class VAE_Checkpoint_QwenImage_Config(Checkpoint_Config_Base, Config_Base): + """Model config for Qwen Image VAE checkpoint models (AutoencoderKLQwenImage).""" + + type: Literal[ModelType.VAE] = Field(default=ModelType.VAE) + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + base: Literal[BaseModelType.QwenImage] = Field(default=BaseModelType.QwenImage) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + state_dict = mod.load_state_dict() + if not _is_qwen_image_vae(state_dict): + raise NotAMatchError("state dict does not look like a Qwen Image VAE") + + return cls(**override_fields) + + +def _has_anima_vae_keys(state_dict: dict[str | int, Any]) -> bool: + """Check if state dict looks like an Anima QwenImage VAE (AutoencoderKLQwenImage). + + The Anima VAE has a distinctive structure with: + - encoder.downsamples.* (instead of encoder.down_blocks) + - decoder.upsamples.* (instead of decoder.up_blocks) + - decoder.head.* / decoder.middle.* + - Top-level conv1/conv2 weights + """ + required_prefixes = { + "encoder.downsamples.", + "decoder.upsamples.", + "decoder.middle.", + } + return all(any(str(k).startswith(prefix) for k in state_dict) for prefix in required_prefixes) + + +class VAE_Checkpoint_Anima_Config(Checkpoint_Config_Base, Config_Base): + """Model config for Anima QwenImage VAE checkpoint models (AutoencoderKLQwenImage).""" + + type: Literal[ModelType.VAE] = Field(default=ModelType.VAE) + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + base: Literal[BaseModelType.Anima] = Field(default=BaseModelType.Anima) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_file(mod) + + raise_for_override_fields(cls, override_fields) + + state_dict = mod.load_state_dict() + if not _has_anima_vae_keys(state_dict): + raise NotAMatchError("state dict does not look like an Anima QwenImage VAE") + + return cls(**override_fields) + + +class VAE_Diffusers_Config_Base(Diffusers_Config_Base): + """Model config for standalone VAE models (diffusers version).""" + + type: Literal[ModelType.VAE] = Field(default=ModelType.VAE) + format: Literal[ModelFormat.Diffusers] = Field(default=ModelFormat.Diffusers) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + raise_for_class_name( + common_config_paths(mod.path), + { + "AutoencoderKL", + "AutoencoderTiny", + }, + ) + + # Unfortunately it is difficult to distinguish SD1 and SDXL VAEs by config alone, so we may need to + # guess based on name if the config is inconclusive. + override_name = override_fields.get("name") + cls._validate_base(mod, override_name) + + return cls(**override_fields) + + @classmethod + def _validate_base(cls, mod: ModelOnDisk, override_name: str | None = None) -> None: + """Raise `NotAMatch` if the model base does not match this config class.""" + expected_base = cls.model_fields["base"].default + recognized_base = cls._get_base_or_raise(mod, override_name) + if expected_base is not recognized_base: + raise NotAMatchError(f"base is {recognized_base}, not {expected_base}") + + @classmethod + def _config_looks_like_sdxl(cls, config: dict[str, Any]) -> bool: + # Heuristic: These config values that distinguish Stability's SD 1.x VAE from their SDXL VAE. + return config.get("scaling_factor", 0) == 0.13025 and config.get("sample_size") in [512, 1024] + + @classmethod + def _name_looks_like_sdxl(cls, mod: ModelOnDisk, override_name: str | None = None) -> bool: + # Heuristic: SD and SDXL VAE are the same shape (3-channel RGB to 4-channel float scaled down + # by a factor of 8), so we can't necessarily tell them apart by config hyperparameters. Best + # we can do is guess based on name. + return bool(re.search(r"xl\b", override_name or mod.path.name, re.IGNORECASE)) + + @classmethod + def _get_base_or_raise(cls, mod: ModelOnDisk, override_name: str | None = None) -> BaseModelType: + config_dict = get_config_dict_or_raise(common_config_paths(mod.path)) + if cls._config_looks_like_sdxl(config_dict): + return BaseModelType.StableDiffusionXL + elif cls._name_looks_like_sdxl(mod, override_name): + return BaseModelType.StableDiffusionXL + else: + # TODO(psyche): Figure out how to positively identify SD1 here, and raise if we can't. Until then, YOLO. + return BaseModelType.StableDiffusion1 + + +class VAE_Diffusers_SD1_Config(VAE_Diffusers_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusion1] = Field(default=BaseModelType.StableDiffusion1) + + +class VAE_Diffusers_SDXL_Config(VAE_Diffusers_Config_Base, Config_Base): + base: Literal[BaseModelType.StableDiffusionXL] = Field(default=BaseModelType.StableDiffusionXL) + + +class VAE_Diffusers_Flux2_Config(Diffusers_Config_Base, Config_Base): + """Model config for FLUX.2 VAE models in diffusers format (AutoencoderKLFlux2).""" + + type: Literal[ModelType.VAE] = Field(default=ModelType.VAE) + format: Literal[ModelFormat.Diffusers] = Field(default=ModelFormat.Diffusers) + base: Literal[BaseModelType.Flux2] = Field(default=BaseModelType.Flux2) + + @classmethod + def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self: + raise_if_not_dir(mod) + + raise_for_override_fields(cls, override_fields) + + raise_for_class_name( + common_config_paths(mod.path), + { + "AutoencoderKLFlux2", + }, + ) + + return cls(**override_fields) diff --git a/invokeai/backend/model_manager/load/__init__.py b/invokeai/backend/model_manager/load/__init__.py new file mode 100644 index 00000000000..eba7bd16a32 --- /dev/null +++ b/invokeai/backend/model_manager/load/__init__.py @@ -0,0 +1,27 @@ +# Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Development Team +""" +Init file for the model loader. +""" + +from importlib import import_module +from pathlib import Path + +from invokeai.backend.model_manager.load.load_base import LoadedModel, LoadedModelWithoutConfig, ModelLoaderBase +from invokeai.backend.model_manager.load.load_default import ModelLoader +from invokeai.backend.model_manager.load.model_cache.model_cache import ModelCache +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry, ModelLoaderRegistryBase + +# This registers the subclasses that implement loaders of specific model types +loaders = [x.stem for x in Path(Path(__file__).parent, "model_loaders").glob("*.py") if x.stem != "__init__"] +for module in loaders: + import_module(f"{__package__}.model_loaders.{module}") + +__all__ = [ + "LoadedModel", + "LoadedModelWithoutConfig", + "ModelCache", + "ModelLoaderBase", + "ModelLoader", + "ModelLoaderRegistryBase", + "ModelLoaderRegistry", +] diff --git a/invokeai/backend/model_manager/load/load_base.py b/invokeai/backend/model_manager/load/load_base.py new file mode 100644 index 00000000000..4609a2e92ab --- /dev/null +++ b/invokeai/backend/model_manager/load/load_base.py @@ -0,0 +1,146 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +""" +Base class for model loading in InvokeAI. +""" + +from abc import ABC, abstractmethod +from contextlib import contextmanager +from logging import Logger +from pathlib import Path +from typing import Any, Dict, Generator, Optional, Tuple + +import torch + +from invokeai.app.services.config import InvokeAIAppConfig +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.load.model_cache.cache_record import CacheRecord +from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_with_partial_load import ( + CachedModelWithPartialLoad, +) +from invokeai.backend.model_manager.load.model_cache.model_cache import ModelCache +from invokeai.backend.model_manager.taxonomy import AnyModel, SubModelType + + +class LoadedModelWithoutConfig: + """Context manager object that mediates transfer from RAM<->VRAM. + + This is a context manager object that has two distinct APIs: + + 1. Older API (deprecated): + Use the LoadedModel object directly as a context manager. It will move the model into VRAM (on CUDA devices), and + return the model in a form suitable for passing to torch. + Example: + ``` + loaded_model_= loader.get_model_by_key('f13dd932', SubModelType('vae')) + with loaded_model as vae: + image = vae.decode(latents)[0] + ``` + + 2. Newer API (recommended): + Call the LoadedModel's `model_on_device()` method in a context. It returns a tuple consisting of a copy of the + model's state dict in CPU RAM followed by a copy of the model in VRAM. The state dict is provided to allow LoRAs and + other model patchers to return the model to its unpatched state without expensive copy and restore operations. + + Example: + ``` + loaded_model_= loader.get_model_by_key('f13dd932', SubModelType('vae')) + with loaded_model.model_on_device() as (state_dict, vae): + image = vae.decode(latents)[0] + ``` + + The state_dict should be treated as a read-only object and never modified. Also be aware that some loadable models + do not have a state_dict, in which case this value will be None. + """ + + def __init__(self, cache_record: CacheRecord, cache: ModelCache): + self._cache_record = cache_record + self._cache = cache + + def __enter__(self) -> AnyModel: + self._cache.lock(self._cache_record, None) + try: + self.repair_required_tensors_on_device() + return self.model + except Exception: + self._cache.unlock(self._cache_record) + raise + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + self._cache.unlock(self._cache_record) + + @contextmanager + def model_on_device( + self, working_mem_bytes: Optional[int] = None + ) -> Generator[Tuple[Optional[Dict[str, torch.Tensor]], AnyModel], None, None]: + """Return a tuple consisting of the model's state dict (if it exists) and the locked model on execution device. + + :param working_mem_bytes: The amount of working memory to keep available on the compute device when loading the + model. + """ + self._cache.lock(self._cache_record, working_mem_bytes) + try: + self.repair_required_tensors_on_device() + yield (self._cache_record.cached_model.get_cpu_state_dict(), self._cache_record.cached_model.model) + finally: + self._cache.unlock(self._cache_record) + + @property + def model(self) -> AnyModel: + """Return the model without locking it.""" + return self._cache_record.cached_model.model + + def repair_required_tensors_on_device(self) -> int: + """Repair required tensors that should be resident on the cached model's execution device.""" + cached_model = self._cache_record.cached_model + if not isinstance(cached_model, CachedModelWithPartialLoad): + return 0 + return cached_model.repair_required_tensors_on_compute_device() + + +class LoadedModel(LoadedModelWithoutConfig): + """Context manager object that mediates transfer from RAM<->VRAM.""" + + def __init__(self, config: Optional[AnyModelConfig], cache_record: CacheRecord, cache: ModelCache): + super().__init__(cache_record=cache_record, cache=cache) + self.config = config + + +class ModelLoaderBase(ABC): + """Abstract base class for loading models into RAM/VRAM.""" + + @abstractmethod + def __init__( + self, + app_config: InvokeAIAppConfig, + logger: Logger, + ram_cache: ModelCache, + ): + """Initialize the loader.""" + pass + + @abstractmethod + def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: + """ + Return a model given its confguration. + + Given a model identified in the model configuration backend, + return a ModelInfo object that can be used to retrieve the model. + + :param model_config: Model configuration, as returned by ModelConfigRecordStore + :param submodel_type: an ModelType enum indicating the portion of + the model to retrieve (e.g. ModelType.Vae) + """ + pass + + @abstractmethod + def get_size_fs( + self, config: AnyModelConfig, model_path: Path, submodel_type: Optional[SubModelType] = None + ) -> int: + """Return size in bytes of the model, calculated before loading.""" + pass + + @property + @abstractmethod + def ram_cache(self) -> ModelCache: + """Return the ram cache associated with this loader.""" + pass diff --git a/invokeai/backend/model_manager/load/load_default.py b/invokeai/backend/model_manager/load/load_default.py new file mode 100644 index 00000000000..040b55cb6ec --- /dev/null +++ b/invokeai/backend/model_manager/load/load_default.py @@ -0,0 +1,312 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Default implementation of model loading in InvokeAI.""" + +import re +from logging import Logger +from pathlib import Path +from typing import Optional + +import torch + +from invokeai.app.services.config import InvokeAIAppConfig +from invokeai.backend.model_manager.configs.base import Diffusers_Config_Base +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.load.load_base import LoadedModel, ModelLoaderBase +from invokeai.backend.model_manager.load.model_cache.cache_record import CacheRecord +from invokeai.backend.model_manager.load.model_cache.model_cache import ModelCache, get_model_cache_key +from invokeai.backend.model_manager.load.model_util import calc_model_size_by_fs +from invokeai.backend.model_manager.load.optimizations import skip_torch_weight_init +from invokeai.backend.model_manager.taxonomy import ( + AnyModel, + SubModelType, +) +from invokeai.backend.util.devices import TorchDevice + +# Layer classes that benefit from FP8 storage. Mirrors diffusers' +# `_GO_LC_SUPPORTED_PYTORCH_LAYERS` so the plain-nn.Module fallback path makes the same +# precision/quality trade-offs as the ModelMixin path. Notably excludes norm and embedding +# wrapper modules — those are handled by their direct param types (Embedding is included +# but pos_embed/patch_embed are filtered by `_FP8_DEFAULT_SKIP_PATTERNS`). +_FP8_SUPPORTED_PYTORCH_LAYERS: tuple[type[torch.nn.Module], ...] = ( + torch.nn.Linear, + torch.nn.Conv1d, + torch.nn.Conv2d, + torch.nn.Conv3d, + torch.nn.ConvTranspose1d, + torch.nn.ConvTranspose2d, + torch.nn.ConvTranspose3d, + torch.nn.Embedding, +) + +# Module-path regexes (matched against `named_modules()` dotted paths) for precision-sensitive +# layers that should never be cast to FP8. Mirrors diffusers' `DEFAULT_SKIP_MODULES_PATTERN` +# — without these, FLUX RMSNorm.scale and similar tiny learned scalars get crushed to FP8 and +# inference quality degrades. Includes anything named `norm`, position/patch embeddings, and +# the in/out projection of transformer blocks. +_FP8_DEFAULT_SKIP_PATTERNS: tuple[str, ...] = ( + "pos_embed", + "patch_embed", + "norm", + r"^proj_in$", + r"^proj_out$", +) + + +# TO DO: The loader is not thread safe! +class ModelLoader(ModelLoaderBase): + """Default implementation of ModelLoaderBase.""" + + def __init__( + self, + app_config: InvokeAIAppConfig, + logger: Logger, + ram_cache: ModelCache, + ): + """Initialize the loader.""" + self._app_config = app_config + self._logger = logger + self._ram_cache = ram_cache + self._torch_dtype = TorchDevice.choose_torch_dtype() + self._torch_device = TorchDevice.choose_torch_device() + + def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: + """ + Return a model given its configuration. + + Given a model's configuration as returned by the ModelRecordConfigStore service, + return a LoadedModel object that can be used for inference. + + :param model config: Configuration record for this model + :param submodel_type: an ModelType enum indicating the portion of + the model to retrieve (e.g. ModelType.Vae) + """ + model_path = self._get_model_path(model_config) + + if not model_path.exists(): + raise FileNotFoundError(f"Files for model '{model_config.name}' not found at {model_path}") + + with skip_torch_weight_init(): + cache_record = self._load_and_cache(model_config, submodel_type) + return LoadedModel(config=model_config, cache_record=cache_record, cache=self._ram_cache) + + @property + def ram_cache(self) -> ModelCache: + """Return the ram cache associated with this loader.""" + return self._ram_cache + + def _get_model_path(self, config: AnyModelConfig) -> Path: + model_base = self._app_config.models_path + return (model_base / config.path).resolve() + + def _get_execution_device( + self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None + ) -> Optional[torch.device]: + """Determine the execution device for a model based on its configuration. + + CPU-only execution is only applied to text encoder submodels to save VRAM while keeping + the denoiser on GPU for performance. Conditioning tensors are moved to GPU after encoding. + + Returns: + torch.device("cpu") if the model should run on CPU only, None otherwise (use cache default). + """ + # Check if this is a text encoder submodel of a main model with cpu_only setting + if hasattr(config, "default_settings") and config.default_settings is not None: + if hasattr(config.default_settings, "cpu_only") and config.default_settings.cpu_only is True: + # Only apply CPU execution to text encoder submodels + if submodel_type in [SubModelType.TextEncoder, SubModelType.TextEncoder2, SubModelType.TextEncoder3]: + return torch.device("cpu") + + # Check if this is a standalone text encoder config with cpu_only field (T5Encoder, Qwen3Encoder, etc.) + if hasattr(config, "cpu_only") and config.cpu_only is True: + return torch.device("cpu") + + return None + + def _load_and_cache(self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> CacheRecord: + stats_name = ":".join([config.base, config.type, config.name, (submodel_type or "")]) + try: + return self._ram_cache.get(key=get_model_cache_key(config.key, submodel_type), stats_name=stats_name) + except IndexError: + pass + + config.path = str(self._get_model_path(config)) + self._ram_cache.make_room(self.get_size_fs(config, Path(config.path), submodel_type)) + loaded_model = self._load_model(config, submodel_type) + + # Determine execution device from model config, considering submodel type + execution_device = self._get_execution_device(config, submodel_type) + + self._ram_cache.put( + get_model_cache_key(config.key, submodel_type), + model=loaded_model, + execution_device=execution_device, + ) + + return self._ram_cache.get(key=get_model_cache_key(config.key, submodel_type), stats_name=stats_name) + + def get_size_fs( + self, config: AnyModelConfig, model_path: Path, submodel_type: Optional[SubModelType] = None + ) -> int: + """Get the size of the model on disk.""" + return calc_model_size_by_fs( + model_path=model_path, + subfolder=submodel_type.value if submodel_type else None, + variant=config.repo_variant if isinstance(config, Diffusers_Config_Base) else None, + ) + + def _should_use_fp8(self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> bool: + """Check if FP8 layerwise casting should be applied to a model.""" + # FP8 storage only works on CUDA + if self._torch_device.type != "cuda": + return False + + # Z-Image has dtype mismatch issues with diffusers' layerwise casting + # (skipped modules produce bf16, hooked modules expect fp16). + from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType + + if hasattr(config, "base") and config.base == BaseModelType.ZImage: + return False + + # VAEs are excluded — fp8 storage causes noticeable quality degradation in decode. + if hasattr(config, "type") and config.type == ModelType.VAE: + return False + + # LoRAs (including ControlLoRA) are excluded — they are not run as a standalone forward pass, + # they are patched into a base model, so the layerwise-casting hooks would never fire. The + # toggle is also hidden in the UI for ControlLoRA; this guard handles legacy persisted values. + if hasattr(config, "type") and config.type in (ModelType.LoRA, ModelType.ControlLoRa): + return False + + # Don't apply FP8 to text encoders, tokenizers, schedulers, VAEs, etc. + _excluded_submodel_types = { + SubModelType.TextEncoder, + SubModelType.TextEncoder2, + SubModelType.TextEncoder3, + SubModelType.Tokenizer, + SubModelType.Tokenizer2, + SubModelType.Tokenizer3, + SubModelType.Scheduler, + SubModelType.SafetyChecker, + SubModelType.VAE, + SubModelType.VAEDecoder, + SubModelType.VAEEncoder, + } + if submodel_type in _excluded_submodel_types: + return False + + # Check default_settings.fp8_storage (Main models, ControlNet) + if hasattr(config, "default_settings") and config.default_settings is not None: + if hasattr(config.default_settings, "fp8_storage") and config.default_settings.fp8_storage is True: + return True + + return False + + def _apply_fp8_layerwise_casting( + self, model: AnyModel, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None + ) -> AnyModel: + """Apply FP8 layerwise casting to a model if enabled in its config.""" + if not self._should_use_fp8(config, submodel_type): + return model + + storage_dtype = torch.float8_e4m3fn + compute_dtype = self._torch_dtype + + # Detect the model's current dtype to use as compute dtype, since models + # (e.g. Flux) may require a specific dtype (bf16) that differs from the global torch dtype (fp16). + if isinstance(model, torch.nn.Module): + first_param = next(model.parameters(), None) + if first_param is not None: + compute_dtype = first_param.dtype + + # We use our own hook-based path for every nn.Module — including diffusers ModelMixin — + # rather than `model.enable_layerwise_casting()`. Diffusers' LayerwiseCastingHook installs + # an instance-level `forward` attribute that captures the original `Linear.forward` in a + # closure. `ModelCache.put()` later runs `apply_custom_layers_to_model`, which constructs a + # new `CustomLinear` sharing the original Linear's `__dict__` — so the diffusers wrapper + # carries over and routes calls back to the captured original forward, silently bypassing + # `CustomLinear.forward` and its `cast_to_device` autocast. With partial loading (e.g. FLUX.2 + # Klein 9B) some weights stay on CPU, the diffusers pre_forward only casts dtype, and + # `F.linear` then sees input on cuda and weight on cpu. Our `register_forward_pre_hook` / + # `register_forward_hook` path fires around `nn.Module._call_impl` without replacing + # `forward`, so `CustomLinear.forward` is still reached. + if isinstance(model, torch.nn.Module): + self._apply_fp8_to_nn_module(model, storage_dtype=storage_dtype, compute_dtype=compute_dtype) + else: + return model + + param_bytes = sum(p.nelement() * p.element_size() for p in model.parameters()) + self._logger.info( + f"FP8 layerwise casting enabled for {config.name} " + f"(storage=float8_e4m3fn, compute={compute_dtype}, " + f"param_size={param_bytes / (1024**2):.0f}MB)" + ) + return model + + @staticmethod + def _apply_fp8_to_nn_module(model: torch.nn.Module, storage_dtype: torch.dtype, compute_dtype: torch.dtype) -> None: + """Apply FP8 layerwise casting to a plain nn.Module. + + Mirrors diffusers' `apply_layerwise_casting` semantics: only the layer classes in + `_FP8_SUPPORTED_PYTORCH_LAYERS` are cast, and modules whose dotted path matches any of + `_FP8_DEFAULT_SKIP_PATTERNS` (norm, pos_embed, patch_embed, proj_in/out) are skipped. + Without the skip list, precision-sensitive tiny learned scalars (e.g. FLUX RMSNorm.scale) + get crushed to FP8 and quality degrades noticeably. + """ + for module_name, module in model.named_modules(): + if not isinstance(module, _FP8_SUPPORTED_PYTORCH_LAYERS): + continue + if any(re.search(pattern, module_name) for pattern in _FP8_DEFAULT_SKIP_PATTERNS): + continue + params = list(module.parameters(recurse=False)) + if not params: + continue + + for param in params: + param.data = param.data.to(storage_dtype) + + ModelLoader._wrap_forward_with_fp8_cast(module, storage_dtype, compute_dtype) + + @staticmethod + def _wrap_forward_with_fp8_cast( + module: torch.nn.Module, storage_dtype: torch.dtype, compute_dtype: torch.dtype + ) -> None: + """Register pre/post forward hooks that cast params to compute dtype on entry and back + to storage dtype on exit. + + We use hooks (rather than overriding `module.forward`) for two reasons: + + 1. **Correct dispatch after `apply_custom_layers_to_model`.** `ModelCache.put()` calls + `apply_custom_layers_to_model`, which creates a NEW `CustomLinear` instance and + shares the original `Linear.__dict__` (see `wrap_custom_layer`). Anything stored in + that dict — including an instance-level `forward` attribute — gets carried over to + the new object. An overridden `forward` would close over the OLD instance, so calls + to the new `CustomLinear` would silently route to `Linear.forward(old_instance, ...)` + and bypass the LoRA-patch-aware branch in `CustomLinear.forward`. Hooks, by contrast, + live in `_forward_hooks` / `_forward_pre_hooks` and are dispatched by + `nn.Module.__call__` with the *actual* called instance — so they run on the new + `CustomLinear` and the class's `forward` is still resolved normally. + + 2. **Exception safety.** `register_forward_hook(..., always_call=True)` fires the + post-hook even when `forward` raises. The plain pre-hook/post-hook pair without + `always_call` would leave params in compute dtype on exception, defeating FP8 + storage savings and making cache size accounting stale. + """ + + def pre_hook(mod: torch.nn.Module, _args: object) -> None: + for p in mod.parameters(recurse=False): + p.data = p.data.to(compute_dtype) + + def post_hook(mod: torch.nn.Module, _args: object, _output: object) -> None: + for p in mod.parameters(recurse=False): + p.data = p.data.to(storage_dtype) + + module.register_forward_pre_hook(pre_hook) + module.register_forward_hook(post_hook, always_call=True) + + # This needs to be implemented in the subclass + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + raise NotImplementedError diff --git a/invokeai/backend/model_manager/load/memory_snapshot.py b/invokeai/backend/model_manager/load/memory_snapshot.py new file mode 100644 index 00000000000..7b693bf8318 --- /dev/null +++ b/invokeai/backend/model_manager/load/memory_snapshot.py @@ -0,0 +1,100 @@ +import gc +from typing import Optional + +import psutil +import torch +from typing_extensions import Self + +from invokeai.backend.model_manager.util.libc_util import LibcUtil, Struct_mallinfo2 + +GB = 2**30 # 1 GB + + +class MemorySnapshot: + """A snapshot of RAM and VRAM usage. All values are in bytes.""" + + def __init__(self, process_ram: int, vram: Optional[int], malloc_info: Optional[Struct_mallinfo2]): + """Initialize a MemorySnapshot. + + Most of the time, `MemorySnapshot` will be constructed with `MemorySnapshot.capture()`. + + Args: + process_ram (int): CPU RAM used by the current process. + vram (Optional[int]): VRAM used by torch. + malloc_info (Optional[Struct_mallinfo2]): Malloc info obtained from LibcUtil. + """ + self.process_ram = process_ram + self.vram = vram + self.malloc_info = malloc_info + + @classmethod + def capture(cls, run_garbage_collector: bool = True) -> Self: + """Capture and return a MemorySnapshot. + + Note: This function has significant overhead, particularly if `run_garbage_collector == True`. + + Args: + run_garbage_collector (bool, optional): If true, gc.collect() will be run before checking the process RAM + usage. Defaults to True. + + Returns: + MemorySnapshot + """ + if run_garbage_collector: + gc.collect() + + # According to the psutil docs (https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_info), rss is + # supported on all platforms. + process_ram = psutil.Process().memory_info().rss + + if torch.cuda.is_available(): + vram = torch.cuda.memory_allocated() + else: + # TODO: We could add support for mps.current_allocated_memory() as well. Leaving out for now until we have + # time to test it properly. + vram = None + + try: + malloc_info = LibcUtil().mallinfo2() + except (OSError, AttributeError): + # OSError: This is expected in environments that do not have the 'libc.so.6' shared library. + # AttributeError: This is expected in environments that have `libc.so.6` but do not have the `mallinfo2` (e.g. glibc < 2.33) + # TODO: Does `mallinfo` work? + malloc_info = None + + return cls(process_ram, vram, malloc_info) + + +def get_pretty_snapshot_diff(snapshot_1: Optional[MemorySnapshot], snapshot_2: Optional[MemorySnapshot]) -> str: + """Get a pretty string describing the difference between two `MemorySnapshot`s.""" + + def get_msg_line(prefix: str, val1: int, val2: int) -> str: + diff = val2 - val1 + return f"{prefix: <30} ({(diff / GB):+5.3f}): {(val1 / GB):5.3f}GB -> {(val2 / GB):5.3f}GB\n" + + msg = "" + + if snapshot_1 is None or snapshot_2 is None: + return msg + + msg += get_msg_line("Process RAM", snapshot_1.process_ram, snapshot_2.process_ram) + + if snapshot_1.malloc_info is not None and snapshot_2.malloc_info is not None: + msg += get_msg_line("libc mmap allocated", snapshot_1.malloc_info.hblkhd, snapshot_2.malloc_info.hblkhd) + + msg += get_msg_line("libc arena used", snapshot_1.malloc_info.uordblks, snapshot_2.malloc_info.uordblks) + + msg += get_msg_line("libc arena free", snapshot_1.malloc_info.fordblks, snapshot_2.malloc_info.fordblks) + + libc_total_allocated_1 = snapshot_1.malloc_info.arena + snapshot_1.malloc_info.hblkhd + libc_total_allocated_2 = snapshot_2.malloc_info.arena + snapshot_2.malloc_info.hblkhd + msg += get_msg_line("libc total allocated", libc_total_allocated_1, libc_total_allocated_2) + + libc_total_used_1 = snapshot_1.malloc_info.uordblks + snapshot_1.malloc_info.hblkhd + libc_total_used_2 = snapshot_2.malloc_info.uordblks + snapshot_2.malloc_info.hblkhd + msg += get_msg_line("libc total used", libc_total_used_1, libc_total_used_2) + + if snapshot_1.vram is not None and snapshot_2.vram is not None: + msg += get_msg_line("VRAM", snapshot_1.vram, snapshot_2.vram) + + return msg diff --git a/invokeai/backend/model_manager/load/model_cache/__init__.py b/invokeai/backend/model_manager/load/model_cache/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/model_manager/load/model_cache/cache_record.py b/invokeai/backend/model_manager/load/model_cache/cache_record.py new file mode 100644 index 00000000000..5b4880a177c --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/cache_record.py @@ -0,0 +1,38 @@ +from dataclasses import dataclass + +from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_only_full_load import ( + CachedModelOnlyFullLoad, +) +from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_with_partial_load import ( + CachedModelWithPartialLoad, +) + + +@dataclass +class CacheRecord: + """A class that represents a model in the model cache.""" + + # Cache key. + key: str + # Model in memory. + cached_model: CachedModelWithPartialLoad | CachedModelOnlyFullLoad + _locks: int = 0 + # Set by ModelCache.drop_model() when the entry was locked at invalidation time. + # ModelCache.unlock() evicts the entry as soon as the last lock releases so a setting + # change (e.g. fp8_storage toggled during an in-flight generation) takes effect on the + # next load instead of silently being ignored. + is_stale: bool = False + + def lock(self) -> None: + """Lock this record.""" + self._locks += 1 + + def unlock(self) -> None: + """Unlock this record.""" + self._locks -= 1 + assert self._locks >= 0 + + @property + def is_locked(self) -> bool: + """Return true if record is locked.""" + return self._locks > 0 diff --git a/invokeai/backend/model_manager/load/model_cache/cache_stats.py b/invokeai/backend/model_manager/load/model_cache/cache_stats.py new file mode 100644 index 00000000000..4998ac6c77a --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/cache_stats.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass, field +from typing import Dict + + +@dataclass +class CacheStats(object): + """Collect statistics on cache performance.""" + + hits: int = 0 # cache hits + misses: int = 0 # cache misses + high_watermark: int = 0 # amount of cache used + in_cache: int = 0 # number of models in cache + cleared: int = 0 # number of models cleared to make space + cache_size: int = 0 # total size of cache + loaded_model_sizes: Dict[str, int] = field(default_factory=dict) diff --git a/invokeai/backend/model_manager/load/model_cache/cached_model/cached_model_only_full_load.py b/invokeai/backend/model_manager/load/model_cache/cached_model/cached_model_only_full_load.py new file mode 100644 index 00000000000..bb04edef9b5 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/cached_model/cached_model_only_full_load.py @@ -0,0 +1,121 @@ +from typing import Any + +import torch + +from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor + + +class CachedModelOnlyFullLoad: + """A wrapper around a PyTorch model to handle full loads and unloads between the CPU and the compute device. + Note: "VRAM" is used throughout this class to refer to the memory on the compute device. It could be CUDA memory, + MPS memory, etc. + """ + + def __init__( + self, model: torch.nn.Module | Any, compute_device: torch.device, total_bytes: int, keep_ram_copy: bool = False + ): + """Initialize a CachedModelOnlyFullLoad. + Args: + model (torch.nn.Module | Any): The model to wrap. Should be on the CPU. + compute_device (torch.device): The compute device to move the model to. + total_bytes (int): The total size (in bytes) of all the weights in the model. + keep_ram_copy (bool): Whether to keep a read-only copy of the model's state dict in RAM. Keeping a RAM copy + increases RAM usage, but speeds up model offload from VRAM and LoRA patching (assuming there is + sufficient RAM). + """ + # model is often a torch.nn.Module, but could be any model type. Throughout this class, we handle both cases. + self._model = model + self._compute_device = compute_device + self._offload_device = torch.device("cpu") + + # A CPU read-only copy of the model's state dict. + self._cpu_state_dict: dict[str, torch.Tensor] | None = None + if isinstance(model, torch.nn.Module) and keep_ram_copy: + self._cpu_state_dict = model.state_dict() + + self._total_bytes = total_bytes + self._is_in_vram = False + + @property + def model(self) -> torch.nn.Module: + return self._model + + def get_cpu_state_dict(self) -> dict[str, torch.Tensor] | None: + """Get a read-only copy of the model's state dict in RAM.""" + # TODO(ryand): Document this better. + return self._cpu_state_dict + + def total_bytes(self) -> int: + """Get the total size (in bytes) of all the weights in the model.""" + return self._total_bytes + + def cur_vram_bytes(self) -> int: + """Get the size (in bytes) of the weights that are currently in VRAM.""" + if self._is_in_vram: + return self._total_bytes + else: + return 0 + + def is_in_vram(self) -> bool: + """Return true if the model is currently in VRAM.""" + return self._is_in_vram + + @property + def compute_device(self) -> torch.device: + """Return the compute device for this model.""" + return self._compute_device + + def full_load_to_vram(self) -> int: + """Load all weights into VRAM (if supported by the model). + Returns: + The number of bytes loaded into VRAM. + """ + if self._is_in_vram: + # Already in VRAM. + return 0 + + if not hasattr(self._model, "to"): + # Model doesn't support moving to a device. + return 0 + + if self._cpu_state_dict is not None: + new_state_dict: dict[str, torch.Tensor] = {} + for k, v in self._cpu_state_dict.items(): + new_state_dict[k] = v.to(self._compute_device, copy=True) + self._model.load_state_dict(new_state_dict, assign=True) + + check_for_gguf = hasattr(self._model, "state_dict") and self._model.state_dict().get("img_in.weight") + if isinstance(check_for_gguf, GGMLTensor): + old_value = torch.__future__.get_overwrite_module_params_on_conversion() + torch.__future__.set_overwrite_module_params_on_conversion(True) + self._model.to(self._compute_device) + torch.__future__.set_overwrite_module_params_on_conversion(old_value) + else: + self._model.to(self._compute_device) + + self._is_in_vram = True + return self._total_bytes + + def full_unload_from_vram(self) -> int: + """Unload all weights from VRAM. + Returns: + The number of bytes unloaded from VRAM. + """ + if not self._is_in_vram: + # Already in RAM. + return 0 + + if self._cpu_state_dict is not None: + self._model.load_state_dict(self._cpu_state_dict, assign=True) + + check_for_gguf = hasattr(self._model, "state_dict") and self._model.state_dict().get("img_in.weight") + if isinstance(check_for_gguf, GGMLTensor): + old_value = torch.__future__.get_overwrite_module_params_on_conversion() + torch.__future__.set_overwrite_module_params_on_conversion(True) + self._model.to(self._offload_device) + torch.__future__.set_overwrite_module_params_on_conversion(old_value) + else: + self._model.to(self._offload_device) + + self._is_in_vram = False + return self._total_bytes diff --git a/invokeai/backend/model_manager/load/model_cache/cached_model/cached_model_with_partial_load.py b/invokeai/backend/model_manager/load/model_cache/cached_model/cached_model_with_partial_load.py new file mode 100644 index 00000000000..328978b45b1 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/cached_model/cached_model_with_partial_load.py @@ -0,0 +1,365 @@ +import torch + +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import ( + CustomModuleMixin, +) +from invokeai.backend.util.calc_tensor_size import calc_tensor_size +from invokeai.backend.util.logging import InvokeAILogger + + +class CachedModelWithPartialLoad: + """A wrapper around a PyTorch model to handle partial loads and unloads between the CPU and the compute device. + + Note: "VRAM" is used throughout this class to refer to the memory on the compute device. It could be CUDA memory, + MPS memory, etc. + """ + + def __init__(self, model: torch.nn.Module, compute_device: torch.device, keep_ram_copy: bool = False): + self._model = model + self._compute_device = compute_device + + model_state_dict = model.state_dict() + # A CPU read-only copy of the model's state dict. Used for faster model unloads from VRAM, and to speed up LoRA + # patching. Set to `None` if keep_ram_copy is False. + self._cpu_state_dict: dict[str, torch.Tensor] | None = model_state_dict if keep_ram_copy else None + + # A dictionary of the size of each tensor in the state dict. + # HACK(ryand): We use this dictionary any time we are doing byte tracking calculations. We do this for + # consistency in case the application code has modified the model's size (e.g. by casting to a different + # precision). Of course, this means that we are making model cache load/unload decisions based on model size + # data that may not be fully accurate. + self._state_dict_bytes = {k: calc_tensor_size(v) for k, v in model_state_dict.items()} + + self._total_bytes = sum(self._state_dict_bytes.values()) + self._cur_vram_bytes: int | None = None + + self._modules_that_support_autocast = self._find_modules_that_support_autocast() + self._keys_in_modules_that_do_not_support_autocast = self._find_keys_in_modules_that_do_not_support_autocast( + model_state_dict + ) + self._state_dict_keys_by_module_prefix = self._group_state_dict_keys_by_module_prefix(model_state_dict) + + def _find_modules_that_support_autocast(self) -> dict[str, torch.nn.Module]: + """Find all modules that support autocasting.""" + return {n: m for n, m in self._model.named_modules() if isinstance(m, CustomModuleMixin)} # type: ignore + + def _find_keys_in_modules_that_do_not_support_autocast(self, state_dict: dict[str, torch.Tensor]) -> set[str]: + keys_in_modules_that_do_not_support_autocast: set[str] = set() + for key in state_dict.keys(): + for module_name in self._modules_that_support_autocast.keys(): + if key.startswith(module_name): + break + else: + keys_in_modules_that_do_not_support_autocast.add(key) + return keys_in_modules_that_do_not_support_autocast + + def _group_state_dict_keys_by_module_prefix(self, state_dict: dict[str, torch.Tensor]) -> dict[str, list[str]]: + """A helper function that groups state dict keys by module prefix. + + Example: + ``` + state_dict = { + "weight": ..., + "module.submodule.weight": ..., + "module.submodule.bias": ..., + "module.other_submodule.weight": ..., + "module.other_submodule.bias": ..., + } + + output = group_state_dict_keys_by_module_prefix(state_dict) + + # The output will be: + output = { + "": [ + "weight", + ], + "module.submodule": [ + "module.submodule.weight", + "module.submodule.bias", + ], + "module.other_submodule": [ + "module.other_submodule.weight", + "module.other_submodule.bias", + ], + } + ``` + """ + state_dict_keys_by_module_prefix: dict[str, list[str]] = {} + for key in state_dict.keys(): + split = key.rsplit(".", 1) + # `split` will have length 1 if the root module has parameters. + module_name = split[0] if len(split) > 1 else "" + if module_name not in state_dict_keys_by_module_prefix: + state_dict_keys_by_module_prefix[module_name] = [] + state_dict_keys_by_module_prefix[module_name].append(key) + return state_dict_keys_by_module_prefix + + def _move_non_persistent_buffers_to_device(self, device: torch.device): + """Move the non-persistent buffers to the target device. These buffers are not included in the state dict, + so we need to move them manually. + """ + # HACK(ryand): Typically, non-persistent buffers are moved when calling module.to(device). We don't move entire + # modules, because we manage the devices of individual tensors using the state dict. Since non-persistent + # buffers are not included in the state dict, we need to handle them manually. The only way to do this is by + # using private torch.nn.Module attributes. + for module in self._model.modules(): + for name, buffer in module.named_buffers(): + if name in module._non_persistent_buffers_set: + module._buffers[name] = buffer.to(device, copy=True) + + def _set_autocast_enabled_in_all_modules(self, enabled: bool): + """Set autocast_enabled flag in all modules that support device autocasting.""" + for module in self._modules_that_support_autocast.values(): + module.set_device_autocasting_enabled(enabled) + + @property + def model(self) -> torch.nn.Module: + return self._model + + def get_cpu_state_dict(self) -> dict[str, torch.Tensor] | None: + """Get a read-only copy of the model's state dict in RAM.""" + # TODO(ryand): Document this better. + return self._cpu_state_dict + + def total_bytes(self) -> int: + """Get the total size (in bytes) of all the weights in the model.""" + return self._total_bytes + + def cur_vram_bytes(self) -> int: + """Get the size (in bytes) of the weights that are currently in VRAM.""" + if self._cur_vram_bytes is None: + cur_state_dict = self._model.state_dict() + self._cur_vram_bytes = sum( + self._state_dict_bytes[k] + for k, v in cur_state_dict.items() + if v.device.type == self._compute_device.type + ) + return self._cur_vram_bytes + + @property + def compute_device(self) -> torch.device: + """Return the compute device for this model.""" + return self._compute_device + + def full_load_to_vram(self) -> int: + """Load all weights into VRAM.""" + return self.partial_load_to_vram(self.total_bytes()) + + def full_unload_from_vram(self) -> int: + """Unload all weights from VRAM.""" + return self.partial_unload_from_vram(self.total_bytes()) + + @torch.no_grad() + def repair_required_tensors_on_compute_device(self) -> int: + """Repair required non-autocast tensors that were left off the compute device. + + This can happen if an interrupted run leaves the model in a partially inconsistent state. Any repaired device + movement invalidates the cached VRAM accounting. + """ + cur_state_dict = self._model.state_dict() + keys_to_repair = { + key + for key in self._keys_in_modules_that_do_not_support_autocast + if cur_state_dict[key].device.type != self._compute_device.type + } + if len(keys_to_repair) == 0: + return 0 + + self._load_state_dict_with_device_conversion(cur_state_dict, keys_to_repair, self._compute_device) + self._move_non_persistent_buffers_to_device(self._compute_device) + self._cur_vram_bytes = None + return len(keys_to_repair) + + def _load_state_dict_with_device_conversion( + self, state_dict: dict[str, torch.Tensor], keys_to_convert: set[str], target_device: torch.device + ): + if self._cpu_state_dict is not None: + # Run the fast version. + self._load_state_dict_with_fast_device_conversion( + state_dict=state_dict, + keys_to_convert=keys_to_convert, + target_device=target_device, + cpu_state_dict=self._cpu_state_dict, + ) + else: + # Run the low-virtual-memory version. + self._load_state_dict_with_jit_device_conversion( + state_dict=state_dict, + keys_to_convert=keys_to_convert, + target_device=target_device, + ) + + def _load_state_dict_with_jit_device_conversion( + self, + state_dict: dict[str, torch.Tensor], + keys_to_convert: set[str], + target_device: torch.device, + ): + """A custom state dict loading implementation with good peak memory properties. + + This implementation has the important property that it copies parameters to the target device one module at a time + rather than applying all of the device conversions and then calling load_state_dict(). This is done to minimize the + peak virtual memory usage. Specifically, we want to avoid a case where we hold references to all of the CPU weights + and CUDA weights simultaneously, because Windows will reserve virtual memory for both. + """ + for module_name, module in self._model.named_modules(): + module_keys = self._state_dict_keys_by_module_prefix.get(module_name, []) + # Calculate the length of the module name prefix. + prefix_len = len(module_name) + if prefix_len > 0: + prefix_len += 1 + + module_state_dict = {} + for key in module_keys: + if key in keys_to_convert: + # It is important that we overwrite `state_dict[key]` to avoid keeping two copies of the same + # parameter. + state_dict[key] = state_dict[key].to(target_device) + # Note that we keep parameters that have not been moved to a new device in case the module implements + # weird custom state dict loading logic that requires all parameters to be present. + module_state_dict[key[prefix_len:]] = state_dict[key] + + if len(module_state_dict) > 0: + # We set strict=False, because if `module` has both parameters and child modules, then we are loading a + # state dict that only contains the parameters of `module` (not its children). + # We assume that it is rare for non-leaf modules to have parameters. Calling load_state_dict() on non-leaf + # modules will recurse through all of the children, so is a bit wasteful. + incompatible_keys = module.load_state_dict(module_state_dict, strict=False, assign=True) + # Missing keys are ok, unexpected keys are not. + assert len(incompatible_keys.unexpected_keys) == 0 + + def _load_state_dict_with_fast_device_conversion( + self, + state_dict: dict[str, torch.Tensor], + keys_to_convert: set[str], + target_device: torch.device, + cpu_state_dict: dict[str, torch.Tensor], + ): + """Convert parameters to the target device and load them into the model. Leverages the `cpu_state_dict` to speed + up transfers of weights to the CPU. + """ + for key in keys_to_convert: + if target_device.type == "cpu": + state_dict[key] = cpu_state_dict[key] + else: + state_dict[key] = state_dict[key].to(target_device) + + self._model.load_state_dict(state_dict, assign=True) + + @torch.no_grad() + def partial_load_to_vram(self, vram_bytes_to_load: int) -> int: + """Load more weights into VRAM without exceeding vram_bytes_to_load. + + Returns: + The number of bytes loaded into VRAM. + """ + # TODO(ryand): Handle the case where an exception is thrown while loading or unloading weights. At the very + # least, we should reset self._cur_vram_bytes to None. + + vram_bytes_loaded = 0 + + cur_state_dict = self._model.state_dict() + + # Identify the keys that will be loaded into VRAM. + keys_to_load: set[str] = set() + + # First, process the keys that *must* be loaded into VRAM. + for key in self._keys_in_modules_that_do_not_support_autocast: + param = cur_state_dict[key] + if param.device.type == self._compute_device.type: + continue + + keys_to_load.add(key) + param_size = self._state_dict_bytes[key] + vram_bytes_loaded += param_size + + if vram_bytes_loaded > vram_bytes_to_load: + logger = InvokeAILogger.get_logger() + logger.warning( + f"Loading {vram_bytes_loaded / 2**20} MB into VRAM, but only {vram_bytes_to_load / 2**20} MB were " + "requested. This is the minimum set of weights in VRAM required to run the model." + ) + + # Next, process the keys that can optionally be loaded into VRAM. + fully_loaded = True + for key, param in cur_state_dict.items(): + # Skip the keys that have already been processed above. + if key in keys_to_load: + continue + + if param.device.type == self._compute_device.type: + continue + + param_size = self._state_dict_bytes[key] + if vram_bytes_loaded + param_size > vram_bytes_to_load: + # TODO(ryand): Should we just break here? If we couldn't fit this parameter into VRAM, is it really + # worth continuing to search for a smaller parameter that would fit? + fully_loaded = False + continue + + keys_to_load.add(key) + vram_bytes_loaded += param_size + + if len(keys_to_load) > 0: + # We load the entire state dict, not just the parameters that changed, in case there are modules that + # override _load_from_state_dict() and do some funky stuff that requires the entire state dict. + # Alternatively, in the future, grouping parameters by module could probably solve this problem. + self._load_state_dict_with_device_conversion(cur_state_dict, keys_to_load, self._compute_device) + + if self._cur_vram_bytes is not None: + self._cur_vram_bytes += vram_bytes_loaded + + if fully_loaded: + self._set_autocast_enabled_in_all_modules(False) + else: + self._set_autocast_enabled_in_all_modules(True) + + # Move all non-persistent buffers to the compute device. These are a weird edge case and do not participate in + # the vram_bytes_loaded tracking. + self._move_non_persistent_buffers_to_device(self._compute_device) + + return vram_bytes_loaded + + @torch.no_grad() + def partial_unload_from_vram(self, vram_bytes_to_free: int, keep_required_weights_in_vram: bool = False) -> int: + """Unload weights from VRAM until vram_bytes_to_free bytes are freed. Or the entire model is unloaded. + + :param keep_required_weights_in_vram: If True, any weights that must be kept in VRAM to run the model will be + kept in VRAM. + + Returns: + The number of bytes unloaded from VRAM. + """ + vram_bytes_freed = 0 + required_weights_in_vram = 0 + + offload_device = "cpu" + cur_state_dict = self._model.state_dict() + + # Identify the keys that will be offloaded to CPU. + keys_to_offload: set[str] = set() + + for key, param in cur_state_dict.items(): + if vram_bytes_freed >= vram_bytes_to_free: + break + + if param.device.type == offload_device: + continue + + if keep_required_weights_in_vram and key in self._keys_in_modules_that_do_not_support_autocast: + required_weights_in_vram += self._state_dict_bytes[key] + continue + + keys_to_offload.add(key) + vram_bytes_freed += self._state_dict_bytes[key] + + if len(keys_to_offload) > 0: + self._load_state_dict_with_device_conversion(cur_state_dict, keys_to_offload, torch.device("cpu")) + + if self._cur_vram_bytes is not None: + self._cur_vram_bytes -= vram_bytes_freed + + # We may have gone from a fully-loaded model to a partially-loaded model, so we need to reapply the custom + # layers. + self._set_autocast_enabled_in_all_modules(True) + return vram_bytes_freed diff --git a/invokeai/backend/model_manager/load/model_cache/dev_utils.py b/invokeai/backend/model_manager/load/model_cache/dev_utils.py new file mode 100644 index 00000000000..4e1bac68917 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/dev_utils.py @@ -0,0 +1,33 @@ +from contextlib import contextmanager + +import torch + +from invokeai.backend.util.logging import InvokeAILogger + + +@contextmanager +def log_operation_vram_usage(operation_name: str): + """A helper function for tuning working memory requirements for memory-intensive ops. + + Sample usage: + + ```python + with log_operation_vram_usage("some_operation"): + some_operation() + ``` + """ + torch.cuda.synchronize() + torch.cuda.reset_peak_memory_stats() + max_allocated_before = torch.cuda.max_memory_allocated() + max_reserved_before = torch.cuda.max_memory_reserved() + try: + yield + finally: + torch.cuda.synchronize() + max_allocated_after = torch.cuda.max_memory_allocated() + max_reserved_after = torch.cuda.max_memory_reserved() + logger = InvokeAILogger.get_logger() + logger.info( + f">>>{operation_name} Peak VRAM allocated: {(max_allocated_after - max_allocated_before) / 2**20} MB, " + f"Peak VRAM reserved: {(max_reserved_after - max_reserved_before) / 2**20} MB" + ) diff --git a/invokeai/backend/model_manager/load/model_cache/model_cache.py b/invokeai/backend/model_manager/load/model_cache/model_cache.py new file mode 100644 index 00000000000..e3a0928e52b --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/model_cache.py @@ -0,0 +1,931 @@ +import gc +import logging +import threading +import time +from dataclasses import dataclass +from functools import wraps +from logging import Logger +from typing import Any, Callable, Dict, List, Optional, Protocol + +import psutil +import torch + +from invokeai.backend.model_manager.load.memory_snapshot import MemorySnapshot +from invokeai.backend.model_manager.load.model_cache.cache_record import CacheRecord +from invokeai.backend.model_manager.load.model_cache.cache_stats import CacheStats +from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_only_full_load import ( + CachedModelOnlyFullLoad, +) +from invokeai.backend.model_manager.load.model_cache.cached_model.cached_model_with_partial_load import ( + CachedModelWithPartialLoad, +) +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.torch_module_autocast import ( + apply_custom_layers_to_model, +) +from invokeai.backend.model_manager.load.model_util import calc_model_size_by_data +from invokeai.backend.model_manager.taxonomy import AnyModel, SubModelType +from invokeai.backend.util.devices import TorchDevice +from invokeai.backend.util.logging import InvokeAILogger +from invokeai.backend.util.prefix_logger_adapter import PrefixedLoggerAdapter + +# Size of a GB in bytes. +GB = 2**30 + +# Size of a MB in bytes. +MB = 2**20 + + +# TODO(ryand): Where should this go? The ModelCache shouldn't be concerned with submodels. +def get_model_cache_key(model_key: str, submodel_type: Optional[SubModelType] = None) -> str: + """Get the cache key for a model based on the optional submodel type.""" + if submodel_type: + return f"{model_key}:{submodel_type.value}" + else: + return model_key + + +def synchronized(method: Callable[..., Any]) -> Callable[..., Any]: + """A decorator that applies the class's self._lock to the method.""" + + @wraps(method) + def wrapper(self, *args, **kwargs): + with self._lock: # Automatically acquire and release the lock + return method(self, *args, **kwargs) + + return wrapper + + +def record_activity(method: Callable[..., Any]) -> Callable[..., Any]: + """A decorator that records activity after a method completes successfully. + + Note: This decorator should be applied to methods that already hold self._lock. + """ + + @wraps(method) + def wrapper(self, *args, **kwargs): + result = method(self, *args, **kwargs) + self._record_activity() + return result + + return wrapper + + +@dataclass +class CacheEntrySnapshot: + cache_key: str + total_bytes: int + current_vram_bytes: int + + +class CacheMissCallback(Protocol): + def __call__( + self, + model_key: str, + cache_snapshot: dict[str, CacheEntrySnapshot], + ) -> None: ... + + +class CacheHitCallback(Protocol): + def __call__( + self, + model_key: str, + cache_snapshot: dict[str, CacheEntrySnapshot], + ) -> None: ... + + +class CacheModelsClearedCallback(Protocol): + def __call__( + self, + models_cleared: int, + bytes_requested: int, + bytes_freed: int, + cache_snapshot: dict[str, CacheEntrySnapshot], + ) -> None: ... + + +class ModelCache: + """A cache for managing models in memory. + + The cache is based on two levels of model storage: + - execution_device: The device where most models are executed (typically "cuda", "mps", or "cpu"). + - storage_device: The device where models are offloaded when not in active use (typically "cpu"). + + The model cache is based on the following assumptions: + - storage_device_mem_size > execution_device_mem_size + - disk_to_storage_device_transfer_time >> storage_device_to_execution_device_transfer_time + + A copy of all models in the cache is always kept on the storage_device. A subset of the models also have a copy on + the execution_device. + + Models are moved between the storage_device and the execution_device as necessary. Cache size limits are enforced + on both the storage_device and the execution_device. The execution_device cache uses a smallest-first offload + policy. The storage_device cache uses a least-recently-used (LRU) offload policy. + + Note: Neither of these offload policies has really been compared against alternatives. It's likely that different + policies would be better, although the optimal policies are likely heavily dependent on usage patterns and HW + configuration. + + The cache returns context manager generators designed to load the model into the execution device (often GPU) within + the context, and unload outside the context. + + Example usage: + ``` + cache = ModelCache(max_cache_size=7.5, max_vram_cache_size=6.0) + with cache.get_model('runwayml/stable-diffusion-1-5') as SD1: + do_something_on_gpu(SD1) + ``` + """ + + def __init__( + self, + execution_device_working_mem_gb: float, + enable_partial_loading: bool, + keep_ram_copy_of_weights: bool, + max_ram_cache_size_gb: float | None = None, + max_vram_cache_size_gb: float | None = None, + execution_device: torch.device | str = "cuda", + storage_device: torch.device | str = "cpu", + log_memory_usage: bool = False, + logger: Optional[Logger] = None, + keep_alive_minutes: float = 0, + ): + """Initialize the model RAM cache. + + :param execution_device_working_mem_gb: The amount of working memory to keep on the GPU (in GB) i.e. non-model + VRAM. + :param enable_partial_loading: Whether to enable partial loading of models. + :param max_ram_cache_size_gb: The maximum amount of CPU RAM to use for model caching in GB. This parameter is + kept to maintain compatibility with previous versions of the model cache, but should be deprecated in the + future. If set, this parameter overrides the default cache size logic. + :param max_vram_cache_size_gb: The amount of VRAM to use for model caching in GB. This parameter is kept to + maintain compatibility with previous versions of the model cache, but should be deprecated in the future. + If set, this parameter overrides the default cache size logic. + :param execution_device: Torch device to load active model into [torch.device('cuda')] + :param storage_device: Torch device to save inactive model in [torch.device('cpu')] + :param 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 disable this feature unless you are actively inspecting the model cache's + behaviour. + :param logger: InvokeAILogger to use (otherwise creates one) + :param keep_alive_minutes: How long to keep models in cache after last use (in minutes). 0 means keep indefinitely. + """ + self._enable_partial_loading = enable_partial_loading + self._keep_ram_copy_of_weights = keep_ram_copy_of_weights + self._execution_device_working_mem_gb = execution_device_working_mem_gb + self._execution_device: torch.device = torch.device(execution_device) + self._storage_device: torch.device = torch.device(storage_device) + + self._max_ram_cache_size_gb = max_ram_cache_size_gb + self._max_vram_cache_size_gb = max_vram_cache_size_gb + + self._logger = PrefixedLoggerAdapter( + logger or InvokeAILogger.get_logger(self.__class__.__name__), "MODEL CACHE" + ) + self._log_memory_usage = log_memory_usage + self._stats: Optional[CacheStats] = None + + self._cached_models: Dict[str, CacheRecord] = {} + self._cache_stack: List[str] = [] + + self._ram_cache_size_bytes = self._calc_ram_available_to_model_cache() + + # A lock applied to all public method calls to make the ModelCache thread-safe. + # At the time of writing, the ModelCache should only be accessed from two threads: + # - The graph execution thread + # - Requests to empty the cache from a separate thread + self._lock = threading.RLock() + + self._on_cache_hit_callbacks: set[CacheHitCallback] = set() + self._on_cache_miss_callbacks: set[CacheMissCallback] = set() + self._on_cache_models_cleared_callbacks: set[CacheModelsClearedCallback] = set() + + # Keep-alive timeout support + self._keep_alive_minutes = keep_alive_minutes + self._last_activity_time: Optional[float] = None + self._timeout_timer: Optional[threading.Timer] = None + self._shutdown_event = threading.Event() + + def on_cache_hit(self, cb: CacheHitCallback) -> Callable[[], None]: + self._on_cache_hit_callbacks.add(cb) + + def unsubscribe() -> None: + self._on_cache_hit_callbacks.discard(cb) + + return unsubscribe + + def on_cache_miss(self, cb: CacheMissCallback) -> Callable[[], None]: + self._on_cache_miss_callbacks.add(cb) + + def unsubscribe() -> None: + self._on_cache_miss_callbacks.discard(cb) + + return unsubscribe + + def on_cache_models_cleared(self, cb: CacheModelsClearedCallback) -> Callable[[], None]: + self._on_cache_models_cleared_callbacks.add(cb) + + def unsubscribe() -> None: + self._on_cache_models_cleared_callbacks.discard(cb) + + return unsubscribe + + @property + @synchronized + def stats(self) -> Optional[CacheStats]: + """Return collected CacheStats object.""" + return self._stats + + @stats.setter + @synchronized + def stats(self, stats: CacheStats) -> None: + """Set the CacheStats object for collecting cache statistics.""" + self._stats = stats + # Populate the cache size in the stats object when it's set + if self._stats is not None: + self._stats.cache_size = self._ram_cache_size_bytes + + def _record_activity(self) -> None: + """Record model activity and reset the timeout timer if configured. + + Note: This method should only be called when self._lock is already held. + """ + if self._keep_alive_minutes <= 0: + return + + self._last_activity_time = time.time() + + # Cancel any existing timer + if self._timeout_timer is not None: + self._timeout_timer.cancel() + + # Start a new timer + timeout_seconds = self._keep_alive_minutes * 60 + self._timeout_timer = threading.Timer(timeout_seconds, self._on_timeout) + # Set as daemon so it doesn't prevent application shutdown + self._timeout_timer.daemon = True + self._timeout_timer.start() + self._logger.debug(f"Model cache activity recorded. Timeout set to {self._keep_alive_minutes} minutes.") + + @synchronized + @record_activity + def _on_timeout(self) -> None: + """Called when the keep-alive timeout expires. Clears the model cache.""" + if self._shutdown_event.is_set(): + return + + # Double-check if there has been activity since the timer was set + # This handles the race condition where activity occurred just before the timer fired + if self._last_activity_time is not None and self._keep_alive_minutes > 0: + elapsed_minutes = (time.time() - self._last_activity_time) / 60 + if elapsed_minutes < self._keep_alive_minutes: + # Activity occurred, don't clear cache + self._logger.debug( + f"Model cache timeout fired but activity detected {elapsed_minutes:.2f} minutes ago. " + f"Skipping cache clear." + ) + return + + # Check if there are any unlocked models that can be cleared + unlocked_models = [key for key, entry in self._cached_models.items() if not entry.is_locked] + + if len(unlocked_models) > 0: + self._logger.info( + f"Model cache keep-alive timeout of {self._keep_alive_minutes} minutes expired. " + f"Clearing {len(unlocked_models)} unlocked model(s) from cache." + ) + # Clear the cache by requesting a very large amount of space. + # This is the same logic used by the "Clear Model Cache" button. + # Using 1000 GB ensures all unlocked models are removed. + self._make_room_internal(1000 * GB) + elif len(self._cached_models) > 0: + # All models are locked, don't log at info level + self._logger.debug( + f"Model cache timeout fired but all {len(self._cached_models)} model(s) are locked. " + f"Skipping cache clear." + ) + else: + self._logger.debug("Model cache timeout fired but cache is already empty.") + + @synchronized + def shutdown(self) -> None: + """Shutdown the model cache, cancelling any pending timers.""" + self._shutdown_event.set() + if self._timeout_timer is not None: + self._timeout_timer.cancel() + self._timeout_timer = None + + @synchronized + @record_activity + def put(self, key: str, model: AnyModel, execution_device: Optional[torch.device] = None) -> None: + """Add a model to the cache. + + Args: + key: Cache key for the model + model: The model to cache + execution_device: Optional device to use for this specific model. If None, uses the cache's default + execution_device. Use torch.device("cpu") to force a model to run on CPU. + """ + if key in self._cached_models: + self._logger.debug( + f"Attempted to add model {key} ({model.__class__.__name__}), but it already exists in the cache. No action necessary." + ) + return + + size = calc_model_size_by_data(self._logger, model) + self._make_room_internal(size) + + # Inject custom modules into the model. + if isinstance(model, torch.nn.Module): + apply_custom_layers_to_model(model) + + # Use the provided execution device, or fall back to the cache's default + effective_execution_device = execution_device if execution_device is not None else self._execution_device + + # Partial loading only makes sense on CUDA. + # - When running on CPU, there is no 'loading' to do. + # - When running on MPS, memory is shared with the CPU, so the default OS memory management already handles this + # well. + running_with_cuda = effective_execution_device.type == "cuda" + + # Wrap model. + if isinstance(model, torch.nn.Module) and running_with_cuda and self._enable_partial_loading: + wrapped_model = CachedModelWithPartialLoad( + model, effective_execution_device, keep_ram_copy=self._keep_ram_copy_of_weights + ) + else: + wrapped_model = CachedModelOnlyFullLoad( + model, effective_execution_device, size, keep_ram_copy=self._keep_ram_copy_of_weights + ) + + cache_record = CacheRecord(key=key, cached_model=wrapped_model) + self._cached_models[key] = cache_record + self._cache_stack.append(key) + self._logger.debug( + f"Added model {key} (Type: {model.__class__.__name__}, Wrap mode: {wrapped_model.__class__.__name__}, Model size: {size / MB:.2f}MB)" + ) + + @synchronized + def _get_cache_snapshot(self) -> dict[str, CacheEntrySnapshot]: + overview: dict[str, CacheEntrySnapshot] = {} + for cache_key, cache_entry in self._cached_models.items(): + total_bytes = cache_entry.cached_model.total_bytes() + current_vram_bytes = cache_entry.cached_model.cur_vram_bytes() + overview[cache_key] = CacheEntrySnapshot( + cache_key=cache_key, + total_bytes=total_bytes, + current_vram_bytes=current_vram_bytes, + ) + + return overview + + @synchronized + @record_activity + def get(self, key: str, stats_name: Optional[str] = None) -> CacheRecord: + """Retrieve a model from the cache. + + :param key: Model key + :param stats_name: A human-readable id for the model for the purposes of stats reporting. + + Raises IndexError if the model is not in the cache. + """ + if key in self._cached_models: + if self.stats: + self.stats.hits += 1 + else: + for cb in self._on_cache_miss_callbacks: + cb(model_key=key, cache_snapshot=self._get_cache_snapshot()) + if self.stats: + self.stats.misses += 1 + self._logger.debug(f"Cache miss: {key}") + raise IndexError(f"The model with key {key} is not in the cache.") + + cache_entry = self._cached_models[key] + + # more stats + if self.stats: + stats_name = stats_name or key + self.stats.high_watermark = max(self.stats.high_watermark, self._get_ram_in_use()) + self.stats.in_cache = len(self._cached_models) + self.stats.loaded_model_sizes[stats_name] = max( + self.stats.loaded_model_sizes.get(stats_name, 0), cache_entry.cached_model.total_bytes() + ) + + # This moves the entry to the top (right end) of the stack. + self._cache_stack = [k for k in self._cache_stack if k != key] + self._cache_stack.append(key) + + self._logger.debug(f"Cache hit: {key} (Type: {cache_entry.cached_model.model.__class__.__name__})") + for cb in self._on_cache_hit_callbacks: + cb(model_key=key, cache_snapshot=self._get_cache_snapshot()) + + return cache_entry + + @synchronized + @record_activity + def lock(self, cache_entry: CacheRecord, working_mem_bytes: Optional[int]) -> None: + """Lock a model for use and move it into VRAM.""" + if cache_entry.key not in self._cached_models: + self._logger.info( + f"Locking model cache entry {cache_entry.key} " + f"(Type: {cache_entry.cached_model.model.__class__.__name__}), but it has already been dropped from " + "the RAM cache. This is a sign that the model loading order is non-optimal in the invocation code " + "(See https://github.com/invoke-ai/InvokeAI/issues/7513)." + ) + # cache_entry = self._cached_models[key] + cache_entry.lock() + + self._logger.debug( + f"Locking model {cache_entry.key} (Type: {cache_entry.cached_model.model.__class__.__name__})" + ) + + # Check if the model's specific compute_device is CPU, not just the cache's default execution_device + model_compute_device = cache_entry.cached_model.compute_device + if model_compute_device.type == "cpu": + # Models configured for CPU execution don't need to be loaded into VRAM + self._logger.debug(f"Model {cache_entry.key} is configured for CPU execution, skipping VRAM load") + return + + try: + self._load_locked_model(cache_entry, working_mem_bytes) + self._logger.debug( + f"Finished locking model {cache_entry.key} (Type: {cache_entry.cached_model.model.__class__.__name__})" + ) + except torch.cuda.OutOfMemoryError: + self._logger.warning("Insufficient GPU memory to load model. Aborting") + cache_entry.unlock() + raise + except Exception: + cache_entry.unlock() + raise + + self._log_cache_state() + + @synchronized + @record_activity + def unlock(self, cache_entry: CacheRecord) -> None: + """Unlock a model.""" + if cache_entry.key not in self._cached_models: + self._logger.info( + f"Unlocking model cache entry {cache_entry.key} " + f"(Type: {cache_entry.cached_model.model.__class__.__name__}), but it has already been dropped from " + "the RAM cache. This is a sign that the model loading order is non-optimal in the invocation code " + "(See https://github.com/invoke-ai/InvokeAI/issues/7513)." + ) + # cache_entry = self._cached_models[key] + cache_entry.unlock() + self._logger.debug( + f"Unlocked model {cache_entry.key} (Type: {cache_entry.cached_model.model.__class__.__name__})" + ) + + # If `drop_model()` marked this entry stale (e.g. settings changed while a generation + # was using it), evict now so the next load rebuilds with the new settings rather than + # silently reusing the pre-change cached module. + if cache_entry.is_stale and not cache_entry.is_locked and cache_entry.key in self._cached_models: + bytes_freed = cache_entry.cached_model.total_bytes() + self._delete_cache_entry(cache_entry) + if self.stats: + self.stats.cleared = (self.stats.cleared or 0) + 1 + snapshot = self._get_cache_snapshot() + for cb in self._on_cache_models_cleared_callbacks: + cb( + models_cleared=1, + bytes_requested=0, + bytes_freed=bytes_freed, + cache_snapshot=snapshot, + ) + gc.collect() + TorchDevice.empty_cache() + self._logger.debug(f"Evicted stale cache entry {cache_entry.key} after unlock.") + + def _load_locked_model(self, cache_entry: CacheRecord, working_mem_bytes: Optional[int] = None) -> None: + """Helper function for self.lock(). Loads a locked model into VRAM.""" + start_time = time.time() + + # Calculate model_vram_needed, the amount of additional VRAM that will be used if we fully load the model into + # VRAM. + model_cur_vram_bytes = cache_entry.cached_model.cur_vram_bytes() + model_total_bytes = cache_entry.cached_model.total_bytes() + model_vram_needed = model_total_bytes - model_cur_vram_bytes + + vram_available = self._get_vram_available(working_mem_bytes) + self._logger.debug( + f"Before unloading: {self._get_vram_state_str(model_cur_vram_bytes, model_total_bytes, vram_available)}" + ) + + # Make room for the model in VRAM. + # 1. If the model can fit entirely in VRAM, then make enough room for it to be loaded fully. + # 2. If the model can't fit fully into VRAM, then unload all other models and load as much of the model as + # possible. + vram_bytes_freed = self._offload_unlocked_models(model_vram_needed, working_mem_bytes) + self._logger.debug(f"Unloaded models (if necessary): vram_bytes_freed={(vram_bytes_freed / MB):.2f}MB") + + # Check the updated vram_available after offloading. + vram_available = self._get_vram_available(working_mem_bytes) + self._logger.debug( + f"After unloading: {self._get_vram_state_str(model_cur_vram_bytes, model_total_bytes, vram_available)}" + ) + + if vram_available < 0: + # There is insufficient VRAM available. As a last resort, try to unload the model being locked from VRAM, + # as it may still be loaded from a previous use. + vram_bytes_freed_from_own_model = self._move_model_to_ram(cache_entry, -vram_available) + vram_available = self._get_vram_available(working_mem_bytes) + self._logger.debug( + f"Unloaded {vram_bytes_freed_from_own_model / MB:.2f}MB from the model being locked ({cache_entry.key})." + ) + + # Move as much of the model as possible into VRAM. + # For testing, only allow 10% of the model to be loaded into VRAM. + # vram_available = int(model_vram_needed * 0.1) + # We add 1 MB to the available VRAM to account for small errors in memory tracking (e.g. off-by-one). A fully + # loaded model is much faster than a 95% loaded model. + model_bytes_loaded = self._move_model_to_vram(cache_entry, vram_available + MB) + + model_cur_vram_bytes = cache_entry.cached_model.cur_vram_bytes() + vram_available = self._get_vram_available(working_mem_bytes) + loaded_percent = model_cur_vram_bytes / model_total_bytes if model_total_bytes > 0 else 0 + # Use the model's actual compute_device for logging, not the cache's default + model_device = cache_entry.cached_model.compute_device + self._logger.info( + f"Loaded model '{cache_entry.key}' ({cache_entry.cached_model.model.__class__.__name__}) onto " + f"{model_device.type} device in {(time.time() - start_time):.2f}s. " + f"Total model size: {model_total_bytes / MB:.2f}MB, " + f"VRAM: {model_cur_vram_bytes / MB:.2f}MB ({loaded_percent:.1%})" + ) + self._logger.debug( + f"Loaded model onto execution device: model_bytes_loaded={(model_bytes_loaded / MB):.2f}MB, " + ) + self._logger.debug( + f"After loading: {self._get_vram_state_str(model_cur_vram_bytes, model_total_bytes, vram_available)}" + ) + + def _move_model_to_vram(self, cache_entry: CacheRecord, vram_available: int) -> int: + try: + if isinstance(cache_entry.cached_model, CachedModelWithPartialLoad): + return cache_entry.cached_model.partial_load_to_vram(vram_available) + elif isinstance(cache_entry.cached_model, CachedModelOnlyFullLoad): # type: ignore + # Partial load is not supported, so we have not choice but to try and fit it all into VRAM. + return cache_entry.cached_model.full_load_to_vram() + else: + raise ValueError(f"Unsupported cached model type: {type(cache_entry.cached_model)}") + except Exception as e: + if isinstance(e, torch.cuda.OutOfMemoryError): + self._logger.warning("Insufficient GPU memory to load model. Aborting") + # If an exception occurs, the model could be left in a bad state, so we delete it from the cache entirely. + self._delete_cache_entry(cache_entry) + raise + + def _move_model_to_ram(self, cache_entry: CacheRecord, vram_bytes_to_free: int) -> int: + try: + if isinstance(cache_entry.cached_model, CachedModelWithPartialLoad): + return cache_entry.cached_model.partial_unload_from_vram( + vram_bytes_to_free, keep_required_weights_in_vram=cache_entry.is_locked + ) + elif isinstance(cache_entry.cached_model, CachedModelOnlyFullLoad): # type: ignore + return cache_entry.cached_model.full_unload_from_vram() + else: + raise ValueError(f"Unsupported cached model type: {type(cache_entry.cached_model)}") + except Exception: + # If an exception occurs, the model could be left in a bad state, so we delete it from the cache entirely. + self._delete_cache_entry(cache_entry) + raise + + def _get_vram_available(self, working_mem_bytes: Optional[int]) -> int: + """Calculate the amount of additional VRAM available for the cache to use (takes into account the working + memory). + """ + # If self._max_vram_cache_size_gb is set, then it overrides the default logic. + if self._max_vram_cache_size_gb is not None: + vram_total_available_to_cache = int(self._max_vram_cache_size_gb * GB) + return vram_total_available_to_cache - self._get_vram_in_use() + + working_mem_bytes_default = int(self._execution_device_working_mem_gb * GB) + working_mem_bytes = max(working_mem_bytes or working_mem_bytes_default, working_mem_bytes_default) + + if self._execution_device.type == "cuda": + # TODO(ryand): It is debatable whether we should use memory_reserved() or memory_allocated() here. + # memory_reserved() includes memory reserved by the torch CUDA memory allocator that may or may not be + # re-used for future allocations. For now, we use memory_allocated() to be conservative. + # vram_reserved = torch.cuda.memory_reserved(self._execution_device) + vram_allocated = torch.cuda.memory_allocated(self._execution_device) + vram_free, _vram_total = torch.cuda.mem_get_info(self._execution_device) + vram_available_to_process = vram_free + vram_allocated + elif self._execution_device.type == "mps": + vram_reserved = torch.mps.driver_allocated_memory() + # TODO(ryand): Is it accurate that MPS shares memory with the CPU? + vram_free = psutil.virtual_memory().available + vram_available_to_process = vram_free + vram_reserved + else: + raise ValueError(f"Unsupported execution device: {self._execution_device.type}") + + vram_total_available_to_cache = vram_available_to_process - working_mem_bytes + vram_cur_available_to_cache = vram_total_available_to_cache - self._get_vram_in_use() + return vram_cur_available_to_cache + + def _get_vram_in_use(self) -> int: + """Get the amount of VRAM currently in use by the cache.""" + if self._execution_device.type == "cuda": + return torch.cuda.memory_allocated() + elif self._execution_device.type == "mps": + return torch.mps.current_allocated_memory() + else: + raise ValueError(f"Unsupported execution device type: {self._execution_device.type}") + # Alternative definition of VRAM in use: + # return sum(ce.cached_model.cur_vram_bytes() for ce in self._cached_models.values()) + + def _calc_ram_available_to_model_cache(self) -> int: + """Calculate the amount of RAM available for the cache to use.""" + # If self._max_ram_cache_size_gb is set, then it overrides the default logic. + if self._max_ram_cache_size_gb is not None: + self._logger.info(f"Using user-defined RAM cache size: {self._max_ram_cache_size_gb} GB.") + return int(self._max_ram_cache_size_gb * GB) + + # Heuristics for dynamically calculating the RAM cache size, **in order of increasing priority**: + # 1. As an initial default, use 50% of the total RAM for InvokeAI. + # - Assume a 2GB baseline for InvokeAI's non-model RAM usage, and use the rest of the RAM for the model cache. + # 2. On a system with a lot of RAM, users probably don't want InvokeAI to eat up too much RAM. + # There are diminishing returns to storing more and more models. So, we apply an upper bound. (Keep in mind + # that most OSes have some amount of disk caching, which we still benefit from if there is excess memory, + # even if we drop models from the cache.) + # - On systems without a CUDA device, the upper bound is 32GB. + # - On systems with a CUDA device, the upper bound is 1x the amount of VRAM (less the working memory). + # 3. Absolute minimum of 4GB. + + # NOTE(ryand): We explored dynamically adjusting the RAM cache size based on memory pressure (using psutil), but + # decided against it for now, for the following reasons: + # - It was surprisingly difficult to get memory metrics with consistent definitions across OSes. (If you go + # down this path again, don't underestimate the amount of complexity here and be sure to test rigorously on all + # OSes.) + # - Making the RAM cache size dynamic opens the door for performance regressions that are hard to diagnose and + # hard for users to understand. It is better for users to see that their RAM is maxed out, and then override + # the default value if desired. + + # Lookup the total VRAM size for the CUDA execution device. + total_cuda_vram_bytes: int | None = None + if self._execution_device.type == "cuda": + _, total_cuda_vram_bytes = torch.cuda.mem_get_info(self._execution_device) + + # Apply heuristic 1. + # ------------------ + heuristics_applied = [1] + total_system_ram_bytes = psutil.virtual_memory().total + # Assumed baseline RAM used by InvokeAI for non-model stuff. + baseline_ram_used_by_invokeai = 2 * GB + ram_available_to_model_cache = int(total_system_ram_bytes * 0.5 - baseline_ram_used_by_invokeai) + + # Apply heuristic 2. + # ------------------ + max_ram_cache_size_bytes = 32 * GB + if total_cuda_vram_bytes is not None: + if self._max_vram_cache_size_gb is not None: + max_ram_cache_size_bytes = int(self._max_vram_cache_size_gb * GB) + else: + max_ram_cache_size_bytes = total_cuda_vram_bytes - int(self._execution_device_working_mem_gb * GB) + if ram_available_to_model_cache > max_ram_cache_size_bytes: + heuristics_applied.append(2) + ram_available_to_model_cache = max_ram_cache_size_bytes + + # Apply heuristic 3. + # ------------------ + if ram_available_to_model_cache < 4 * GB: + heuristics_applied.append(3) + ram_available_to_model_cache = 4 * GB + + self._logger.info( + f"Calculated model RAM cache size: {ram_available_to_model_cache / MB:.2f} MB. Heuristics applied: {heuristics_applied}." + ) + return ram_available_to_model_cache + + def _get_ram_in_use(self) -> int: + """Get the amount of RAM currently in use.""" + return sum(ce.cached_model.total_bytes() for ce in self._cached_models.values()) + + def _get_ram_available(self) -> int: + """Get the amount of RAM available for the cache to use.""" + return self._ram_cache_size_bytes - self._get_ram_in_use() + + def _capture_memory_snapshot(self) -> Optional[MemorySnapshot]: + if self._log_memory_usage: + return MemorySnapshot.capture() + return None + + def _get_vram_state_str(self, model_cur_vram_bytes: int, model_total_bytes: int, vram_available: int) -> str: + """Helper function for preparing a VRAM state log string.""" + model_cur_vram_bytes_percent = model_cur_vram_bytes / model_total_bytes if model_total_bytes > 0 else 0 + return ( + f"model_total={model_total_bytes / MB:.0f} MB, " + + f"model_vram={model_cur_vram_bytes / MB:.0f} MB ({model_cur_vram_bytes_percent:.1%} %), " + # + f"vram_total={int(self._max_vram_cache_size * GB)/MB:.0f} MB, " + + f"vram_available={(vram_available / MB):.0f} MB, " + ) + + def _offload_unlocked_models(self, vram_bytes_required: int, working_mem_bytes: Optional[int] = None) -> int: + """Offload models from the execution_device until vram_bytes_required bytes are available, or all models are + offloaded. Of course, locked models are not offloaded. + + Returns: + int: The number of bytes freed based on believed model sizes. The actual change in VRAM may be different. + """ + self._logger.debug( + f"Offloading unlocked models with goal of making room for {vram_bytes_required / MB:.2f}MB of VRAM." + ) + vram_bytes_freed = 0 + # TODO(ryand): Give more thought to the offloading policy used here. + cache_entries_increasing_size = sorted(self._cached_models.values(), key=lambda x: x.cached_model.total_bytes()) + for cache_entry in cache_entries_increasing_size: + # We do not fully trust the count of bytes freed, so we check again on each iteration. + vram_available = self._get_vram_available(working_mem_bytes) + vram_bytes_to_free = vram_bytes_required - vram_available + if vram_bytes_to_free <= 0: + break + if cache_entry.is_locked: + # TODO(ryand): In the future, we may want to partially unload locked models, but this requires careful + # handling of model patches (e.g. LoRA). + continue + cache_entry_bytes_freed = self._move_model_to_ram(cache_entry, vram_bytes_to_free) + if cache_entry_bytes_freed > 0: + self._logger.debug( + f"Unloaded {cache_entry.key} from VRAM to free {(cache_entry_bytes_freed / MB):.0f} MB." + ) + vram_bytes_freed += cache_entry_bytes_freed + + TorchDevice.empty_cache() + return vram_bytes_freed + + def _log_cache_state(self, title: str = "Model cache state:", include_entry_details: bool = True): + if self._logger.getEffectiveLevel() > logging.DEBUG: + # Short circuit if the logger is not set to debug. Some of the data lookups could take a non-negligible + # amount of time. + return + + log = f"{title}\n" + + log_format = " {:<30} Limit: {:>7.1f} MB, Used: {:>7.1f} MB ({:>5.1%}), Available: {:>7.1f} MB ({:>5.1%})\n" + + ram_in_use_bytes = self._get_ram_in_use() + ram_available_bytes = self._get_ram_available() + ram_size_bytes = ram_in_use_bytes + ram_available_bytes + ram_in_use_bytes_percent = ram_in_use_bytes / ram_size_bytes if ram_size_bytes > 0 else 0 + ram_available_bytes_percent = ram_available_bytes / ram_size_bytes if ram_size_bytes > 0 else 0 + log += log_format.format( + f"Storage Device ({self._storage_device.type})", + ram_size_bytes / MB, + ram_in_use_bytes / MB, + ram_in_use_bytes_percent, + ram_available_bytes / MB, + ram_available_bytes_percent, + ) + + if self._execution_device.type != "cpu": + vram_in_use_bytes = self._get_vram_in_use() + vram_available_bytes = self._get_vram_available(None) + vram_size_bytes = vram_in_use_bytes + vram_available_bytes + vram_in_use_bytes_percent = vram_in_use_bytes / vram_size_bytes if vram_size_bytes > 0 else 0 + vram_available_bytes_percent = vram_available_bytes / vram_size_bytes if vram_size_bytes > 0 else 0 + log += log_format.format( + f"Compute Device ({self._execution_device.type})", + vram_size_bytes / MB, + vram_in_use_bytes / MB, + vram_in_use_bytes_percent, + vram_available_bytes / MB, + vram_available_bytes_percent, + ) + + if torch.cuda.is_available(): + log += " {:<30} {:.1f} MB\n".format("CUDA Memory Allocated:", torch.cuda.memory_allocated() / MB) + log += " {:<30} {}\n".format("Total models:", len(self._cached_models)) + + if include_entry_details and len(self._cached_models) > 0: + log += " Models:\n" + log_format = ( + " {:<80} total={:>7.1f} MB, vram={:>7.1f} MB ({:>5.1%}), ram={:>7.1f} MB ({:>5.1%}), locked={}\n" + ) + for cache_record in self._cached_models.values(): + total_bytes = cache_record.cached_model.total_bytes() + cur_vram_bytes = cache_record.cached_model.cur_vram_bytes() + cur_vram_bytes_percent = cur_vram_bytes / total_bytes if total_bytes > 0 else 0 + cur_ram_bytes = total_bytes - cur_vram_bytes + cur_ram_bytes_percent = cur_ram_bytes / total_bytes if total_bytes > 0 else 0 + + log += log_format.format( + f"{cache_record.key} ({cache_record.cached_model.model.__class__.__name__}):", + total_bytes / MB, + cur_vram_bytes / MB, + cur_vram_bytes_percent, + cur_ram_bytes / MB, + cur_ram_bytes_percent, + cache_record.is_locked, + ) + + self._logger.debug(log) + + @synchronized + def make_room(self, bytes_needed: int) -> None: + """Make enough room in the cache to accommodate a new model of indicated size. + + Note: This function deletes all of the cache's internal references to a model in order to free it. If there are + external references to the model, there's nothing that the cache can do about it, and those models will not be + garbage-collected. + """ + self._make_room_internal(bytes_needed) + + def _make_room_internal(self, bytes_needed: int) -> None: + """Internal implementation of make_room(). Assumes the lock is already held.""" + self._logger.debug(f"Making room for {bytes_needed / MB:.2f}MB of RAM.") + self._log_cache_state(title="Before dropping models:") + + ram_bytes_available = self._get_ram_available() + ram_bytes_to_free = max(0, bytes_needed - ram_bytes_available) + + ram_bytes_freed = 0 + pos = 0 + models_cleared = 0 + while ram_bytes_freed < ram_bytes_to_free and pos < len(self._cache_stack): + model_key = self._cache_stack[pos] + cache_entry = self._cached_models[model_key] + + if not cache_entry.is_locked: + ram_bytes_freed += cache_entry.cached_model.total_bytes() + self._logger.debug( + f"Dropping {model_key} from RAM cache to free {(cache_entry.cached_model.total_bytes() / MB):.2f}MB." + ) + self._delete_cache_entry(cache_entry) + del cache_entry + models_cleared += 1 + else: + pos += 1 + + if models_cleared > 0: + # There would likely be some 'garbage' to be collected regardless of whether a model was cleared or not, but + # there is a significant time cost to calling `gc.collect()`, so we want to use it sparingly. (The time cost + # is high even if no garbage gets collected.) + # + # Calling gc.collect(...) when a model is cleared seems like a good middle-ground: + # - If models had to be cleared, it's a signal that we are close to our memory limit. + # - If models were cleared, there's a good chance that there's a significant amount of garbage to be + # collected. + # + # Keep in mind that gc is only responsible for handling reference cycles. Most objects should be cleaned up + # immediately when their reference count hits 0. + if self.stats: + self.stats.cleared = models_cleared + for cb in self._on_cache_models_cleared_callbacks: + cb( + models_cleared=models_cleared, + bytes_requested=bytes_needed, + bytes_freed=ram_bytes_freed, + cache_snapshot=self._get_cache_snapshot(), + ) + gc.collect() + + TorchDevice.empty_cache() + self._logger.debug(f"Dropped {models_cleared} models to free {ram_bytes_freed / MB:.2f}MB of RAM.") + self._log_cache_state(title="After dropping models:") + + def _delete_cache_entry(self, cache_entry: CacheRecord) -> None: + """Delete cache_entry from the cache if it exists. No exception is thrown if it doesn't exist.""" + self._cache_stack = [key for key in self._cache_stack if key != cache_entry.key] + self._cached_models.pop(cache_entry.key, None) + + @synchronized + def drop_model(self, model_key: str) -> int: + """Drop all cache entries belonging to a model so the next load rebuilds them. + + Cache keys are `` or `:` (see `get_model_cache_key`), + so a single model may have multiple entries. Locked entries are marked `is_stale` and + evicted by `unlock()` as soon as the last lock releases — without that, a setting + toggled during an in-flight generation would survive on the locked entry and quietly + get reused by the next generation. + + Returns the number of entries immediately dropped (locked entries that are only marked + stale do not count). + """ + prefix = f"{model_key}:" + matching: list[CacheRecord] = [ + entry for key, entry in self._cached_models.items() if key == model_key or key.startswith(prefix) + ] + + dropped: list[CacheRecord] = [] + bytes_freed = 0 + for entry in matching: + if entry.is_locked: + entry.is_stale = True + continue + bytes_freed += entry.cached_model.total_bytes() + self._delete_cache_entry(entry) + dropped.append(entry) + + if dropped: + if self.stats: + self.stats.cleared = len(dropped) + snapshot = self._get_cache_snapshot() + for cb in self._on_cache_models_cleared_callbacks: + cb( + models_cleared=len(dropped), + bytes_requested=0, + bytes_freed=bytes_freed, + cache_snapshot=snapshot, + ) + gc.collect() + TorchDevice.empty_cache() + return len(dropped) diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/__init__.py b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/cast_to_device.py b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/cast_to_device.py new file mode 100644 index 00000000000..7a50a19953b --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/cast_to_device.py @@ -0,0 +1,15 @@ +from typing import TypeVar + +import torch + +T = TypeVar("T", torch.Tensor, None, torch.Tensor | None) + + +def cast_to_device(t: T, to_device: torch.device) -> T: + """Helper function to cast an optional tensor to a target device.""" + if t is None: + return t + + if t.device.type != to_device.type: + return t.to(to_device) + return t diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/README.md b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/README.md new file mode 100644 index 00000000000..cadb1b6dd5a --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/README.md @@ -0,0 +1,8 @@ + +This directory contains custom implementations of common torch.nn.Module classes that add support for: +- Streaming weights to the execution device +- Applying sidecar patches at execution time (e.g. sidecar LoRA layers) + +Each custom class sub-classes the original module type that is is replacing, so the following properties are preserved: +- `isinstance(m, torch.nn.OrginalModule)` should still work. +- Patching the weights directly (e.g. for LoRA) should still work. (Of course, this is not possible for quantized layers, hence the sidecar support.) diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/__init__.py b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_conv1d.py b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_conv1d.py new file mode 100644 index 00000000000..e65b3259246 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_conv1d.py @@ -0,0 +1,43 @@ +import torch + +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import ( + CustomModuleMixin, +) +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.utils import ( + add_nullable_tensors, +) + + +class CustomConv1d(torch.nn.Conv1d, CustomModuleMixin): + def _autocast_forward_with_patches(self, input: torch.Tensor) -> torch.Tensor: + weight = cast_to_device(self.weight, input.device) + bias = cast_to_device(self.bias, input.device) + + # Prepare the original parameters for the patch aggregation. + orig_params = {"weight": weight, "bias": bias} + # Filter out None values. + orig_params = {k: v for k, v in orig_params.items() if v is not None} + + aggregated_param_residuals = self._aggregate_patch_parameters( + patches_and_weights=self._patches_and_weights, + orig_params=orig_params, + device=input.device, + ) + + weight = add_nullable_tensors(weight, aggregated_param_residuals.get("weight", None)) + bias = add_nullable_tensors(bias, aggregated_param_residuals.get("bias", None)) + return self._conv_forward(input, weight, bias) + + def _autocast_forward(self, input: torch.Tensor) -> torch.Tensor: + weight = cast_to_device(self.weight, input.device) + bias = cast_to_device(self.bias, input.device) + return self._conv_forward(input, weight, bias) + + def forward(self, input: torch.Tensor) -> torch.Tensor: + if len(self._patches_and_weights) > 0: + return self._autocast_forward_with_patches(input) + elif self._device_autocasting_enabled: + return self._autocast_forward(input) + else: + return super().forward(input) diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_conv2d.py b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_conv2d.py new file mode 100644 index 00000000000..eac3549b5ab --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_conv2d.py @@ -0,0 +1,74 @@ +import torch + +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import ( + CustomModuleMixin, +) +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.utils import ( + add_nullable_tensors, +) +from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor + + +class CustomConv2d(torch.nn.Conv2d, CustomModuleMixin): + def _cast_tensor_for_input(self, tensor: torch.Tensor | None, input: torch.Tensor) -> torch.Tensor | None: + tensor = cast_to_device(tensor, input.device) + if ( + tensor is not None + and input.is_floating_point() + and tensor.is_floating_point() + and not isinstance(tensor, GGMLTensor) + and tensor.dtype != input.dtype + ): + tensor = tensor.to(dtype=input.dtype) + return tensor + + def _autocast_forward_with_patches(self, input: torch.Tensor) -> torch.Tensor: + weight = self._cast_tensor_for_input(self.weight, input) + bias = self._cast_tensor_for_input(self.bias, input) + + # Prepare the original parameters for the patch aggregation. + orig_params = {"weight": weight, "bias": bias} + # Filter out None values. + orig_params = {k: v for k, v in orig_params.items() if v is not None} + + aggregated_param_residuals = self._aggregate_patch_parameters( + patches_and_weights=self._patches_and_weights, + orig_params=orig_params, + device=input.device, + ) + + residual_weight = self._cast_tensor_for_input(aggregated_param_residuals.get("weight", None), input) + residual_bias = self._cast_tensor_for_input(aggregated_param_residuals.get("bias", None), input) + weight = add_nullable_tensors(weight, residual_weight) + bias = add_nullable_tensors(bias, residual_bias) + return self._conv_forward(input, weight, bias) + + def _autocast_forward(self, input: torch.Tensor) -> torch.Tensor: + weight = self._cast_tensor_for_input(self.weight, input) + bias = self._cast_tensor_for_input(self.bias, input) + return self._conv_forward(input, weight, bias) + + def forward(self, input: torch.Tensor) -> torch.Tensor: + if len(self._patches_and_weights) > 0: + return self._autocast_forward_with_patches(input) + elif self._device_autocasting_enabled: + return self._autocast_forward(input) + elif input.is_floating_point() and ( + ( + self.weight.is_floating_point() + and not isinstance(self.weight, GGMLTensor) + and self.weight.dtype != input.dtype + ) + or ( + self.bias is not None + and self.bias.is_floating_point() + and not isinstance(self.bias, GGMLTensor) + and self.bias.dtype != input.dtype + ) + ): + weight = self._cast_tensor_for_input(self.weight, input) + bias = self._cast_tensor_for_input(self.bias, input) + return self._conv_forward(input, weight, bias) + else: + return super().forward(input) diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_diffusers_rms_norm.py b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_diffusers_rms_norm.py new file mode 100644 index 00000000000..7aa448d0744 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_diffusers_rms_norm.py @@ -0,0 +1,40 @@ +import torch +from diffusers.models.normalization import RMSNorm as DiffusersRMSNorm + +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import ( + CustomModuleMixin, +) + + +class CustomDiffusersRMSNorm(DiffusersRMSNorm, CustomModuleMixin): + """Custom wrapper for diffusers RMSNorm that supports device autocasting for partial model loading.""" + + def _autocast_forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + weight = cast_to_device(self.weight, hidden_states.device) if self.weight is not None else None + bias = cast_to_device(self.bias, hidden_states.device) if self.bias is not None else None + + input_dtype = hidden_states.dtype + variance = hidden_states.to(torch.float32).pow(2).mean(-1, keepdim=True) + hidden_states = hidden_states * torch.rsqrt(variance + self.eps) + + if weight is not None: + # convert into half-precision if necessary + if weight.dtype in [torch.float16, torch.bfloat16]: + hidden_states = hidden_states.to(weight.dtype) + hidden_states = hidden_states * weight + if bias is not None: + hidden_states = hidden_states + bias + else: + hidden_states = hidden_states.to(input_dtype) + + return hidden_states + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + if len(self._patches_and_weights) > 0: + raise RuntimeError("DiffusersRMSNorm layers do not support patches") + + if self._device_autocasting_enabled: + return self._autocast_forward(hidden_states) + else: + return super().forward(hidden_states) diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_embedding.py b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_embedding.py new file mode 100644 index 00000000000..e622b678fa4 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_embedding.py @@ -0,0 +1,29 @@ +import torch + +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import ( + CustomModuleMixin, +) + + +class CustomEmbedding(torch.nn.Embedding, CustomModuleMixin): + def _autocast_forward(self, input: torch.Tensor) -> torch.Tensor: + weight = cast_to_device(self.weight, input.device) + return torch.nn.functional.embedding( + input, + weight, + self.padding_idx, + self.max_norm, + self.norm_type, + self.scale_grad_by_freq, + self.sparse, + ) + + def forward(self, input: torch.Tensor) -> torch.Tensor: + if len(self._patches_and_weights) > 0: + raise RuntimeError("Embedding layers do not support patches") + + if self._device_autocasting_enabled: + return self._autocast_forward(input) + else: + return super().forward(input) diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_flux_rms_norm.py b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_flux_rms_norm.py new file mode 100644 index 00000000000..dccbe4af6c7 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_flux_rms_norm.py @@ -0,0 +1,36 @@ +import torch + +from invokeai.backend.flux.modules.layers import RMSNorm +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import ( + CustomModuleMixin, +) +from invokeai.backend.patches.layers.set_parameter_layer import SetParameterLayer + + +class CustomFluxRMSNorm(RMSNorm, CustomModuleMixin): + def _autocast_forward_with_patches(self, x: torch.Tensor) -> torch.Tensor: + # Currently, CustomFluxRMSNorm layers only support patching with a single SetParameterLayer. + assert len(self._patches_and_weights) == 1 + patch, _patch_weight = self._patches_and_weights[0] + assert isinstance(patch, SetParameterLayer) + assert patch.param_name == "scale" + + scale = cast_to_device(patch.weight, x.device) + + # Apply the patch. + # NOTE(ryand): Currently, we ignore the patch weight when running as a sidecar. It's not clear how this should + # be handled. + return torch.nn.functional.rms_norm(x, scale.shape, scale, eps=1e-6) + + def _autocast_forward(self, x: torch.Tensor) -> torch.Tensor: + scale = cast_to_device(self.scale, x.device) + return torch.nn.functional.rms_norm(x, scale.shape, scale, eps=1e-6) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if len(self._patches_and_weights) > 0: + return self._autocast_forward_with_patches(x) + elif self._device_autocasting_enabled: + return self._autocast_forward(x) + else: + return super().forward(x) diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_group_norm.py b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_group_norm.py new file mode 100644 index 00000000000..d02e2d533f1 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_group_norm.py @@ -0,0 +1,22 @@ +import torch + +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import ( + CustomModuleMixin, +) + + +class CustomGroupNorm(torch.nn.GroupNorm, CustomModuleMixin): + def _autocast_forward(self, input: torch.Tensor) -> torch.Tensor: + weight = cast_to_device(self.weight, input.device) + bias = cast_to_device(self.bias, input.device) + return torch.nn.functional.group_norm(input, self.num_groups, weight, bias, self.eps) + + def forward(self, input: torch.Tensor) -> torch.Tensor: + if len(self._patches_and_weights) > 0: + raise RuntimeError("GroupNorm layers do not support patches") + + if self._device_autocasting_enabled: + return self._autocast_forward(input) + else: + return super().forward(input) diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_invoke_linear_8_bit_lt.py b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_invoke_linear_8_bit_lt.py new file mode 100644 index 00000000000..0f538caa5a4 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_invoke_linear_8_bit_lt.py @@ -0,0 +1,66 @@ +import bitsandbytes as bnb +import torch + +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_linear import ( + autocast_linear_forward_sidecar_patches, +) +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import ( + CustomModuleMixin, +) +from invokeai.backend.patches.layers.param_shape_utils import get_param_shape +from invokeai.backend.quantization.bnb_llm_int8 import InvokeLinear8bitLt +from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor + + +class CustomInvokeLinear8bitLt(InvokeLinear8bitLt, CustomModuleMixin): + def _cast_tensor_for_input(self, tensor: torch.Tensor | None, input: torch.Tensor) -> torch.Tensor | None: + tensor = cast_to_device(tensor, input.device) + if ( + tensor is not None + and input.is_floating_point() + and tensor.is_floating_point() + and not isinstance(tensor, GGMLTensor) + and tensor.dtype != input.dtype + ): + tensor = tensor.to(dtype=input.dtype) + return tensor + + def _cast_weight_bias_for_input(self, input: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor | None]: + # See the matching method on CustomInvokeLinearNF4 for the rationale. Int8Params doesn't have + # the same packed-shape problem as Params4bit, but we still substitute a meta tensor so that + # patches don't accidentally read the quantized weight values. + weight = torch.empty(get_param_shape(self.weight), device="meta") + bias = self._cast_tensor_for_input(self.bias, input) + return weight, bias + + def _autocast_forward_with_patches(self, x: torch.Tensor) -> torch.Tensor: + return autocast_linear_forward_sidecar_patches(self, x, self._patches_and_weights) + + def _autocast_forward(self, x: torch.Tensor) -> torch.Tensor: + matmul_state = bnb.MatmulLtState() + matmul_state.threshold = self.state.threshold + matmul_state.has_fp16_weights = self.state.has_fp16_weights + matmul_state.use_pool = self.state.use_pool + matmul_state.is_training = self.training + # The underlying InvokeInt8Params weight must already be quantized. + assert self.weight.CB is not None + matmul_state.CB = cast_to_device(self.weight.CB, x.device) + matmul_state.SCB = cast_to_device(self.weight.SCB, x.device) + + # weights are cast automatically as Int8Params, but the bias has to be cast manually. + if self.bias is not None and self.bias.dtype != x.dtype: + self.bias.data = self.bias.data.to(x.dtype) + + # NOTE(ryand): The second parameter should not be needed at all given our expected inference configuration, but + # it's dtype field must be accessible, even though it's not used. We pass in self.weight even though it could be + # on the wrong device. + return bnb.matmul(x, self.weight, bias=cast_to_device(self.bias, x.device), state=matmul_state) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if len(self._patches_and_weights) > 0: + return self._autocast_forward_with_patches(x) + elif self._device_autocasting_enabled: + return self._autocast_forward(x) + else: + return super().forward(x) diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_invoke_linear_nf4.py b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_invoke_linear_nf4.py new file mode 100644 index 00000000000..82596901704 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_invoke_linear_nf4.py @@ -0,0 +1,86 @@ +import copy + +import bitsandbytes as bnb +import torch + +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_linear import ( + autocast_linear_forward_sidecar_patches, +) +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import ( + CustomModuleMixin, +) +from invokeai.backend.patches.layers.param_shape_utils import get_param_shape +from invokeai.backend.quantization.bnb_nf4 import InvokeLinearNF4 +from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor + + +class CustomInvokeLinearNF4(InvokeLinearNF4, CustomModuleMixin): + def _cast_tensor_for_input(self, tensor: torch.Tensor | None, input: torch.Tensor) -> torch.Tensor | None: + tensor = cast_to_device(tensor, input.device) + if ( + tensor is not None + and input.is_floating_point() + and tensor.is_floating_point() + and not isinstance(tensor, GGMLTensor) + and tensor.dtype != input.dtype + ): + tensor = tensor.to(dtype=input.dtype) + return tensor + + def _cast_weight_bias_for_input(self, input: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor | None]: + # The NF4 weight is a Params4bit whose .shape reports the *packed-byte* layout, not the logical + # (out_features, in_features) shape. We hand patches a meta-device tensor with the correct + # logical shape so that shape-only patches (LoRA, LoHA, MergedLayerPatch over LoRA, ...) work. + # Patches that read the original weight values (e.g. SetParameterLayer, DoRA) are not supported + # on NF4-quantized modules. + weight = torch.empty(get_param_shape(self.weight), device="meta") + bias = self._cast_tensor_for_input(self.bias, input) + return weight, bias + + def _autocast_forward_with_patches(self, x: torch.Tensor) -> torch.Tensor: + return autocast_linear_forward_sidecar_patches(self, x, self._patches_and_weights) + + def _autocast_forward(self, x: torch.Tensor) -> torch.Tensor: + bnb.nn.modules.fix_4bit_weight_quant_state_from_module(self) + + # weights are cast automatically as Int8Params, but the bias has to be cast manually + if self.bias is not None and self.bias.dtype != x.dtype: + self.bias.data = self.bias.data.to(x.dtype) + + if not self.compute_type_is_set: + self.set_compute_type(x) + self.compute_type_is_set = True + + inp_dtype = x.dtype + if self.compute_dtype is not None: + x = x.to(self.compute_dtype) + + bias = None if self.bias is None else self.bias.to(self.compute_dtype) + + # HACK(ryand): Casting self.weight to the device also casts the self.weight.quant_state in-place (i.e. it + # does not follow the tensor semantics of returning a new copy when converting to a different device). This + # means that quant_state elements that started on the CPU would be left on the GPU, which we don't want. To + # avoid this side effect we make a shallow copy of the original quant_state so that we can restore it. Fixing + # this properly would require more invasive changes to the bitsandbytes library. + + # Make a shallow copy of the quant_state so that we can undo the in-place modification that occurs when casting + # to a new device. + old_quant_state = copy.copy(self.weight.quant_state) + weight = cast_to_device(self.weight, x.device) + self.weight.quant_state = old_quant_state + + # For some reason, the quant_state.to(...) implementation fails to cast the quant_state.code field. We do this + # manually here. + weight.quant_state.code = cast_to_device(weight.quant_state.code, x.device) + + bias = cast_to_device(self.bias, x.device) + return bnb.matmul_4bit(x, weight.t(), bias=bias, quant_state=weight.quant_state).to(inp_dtype) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if len(self._patches_and_weights) > 0: + return self._autocast_forward_with_patches(x) + elif self._device_autocasting_enabled: + return self._autocast_forward(x) + else: + return super().forward(x) diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_layer_norm.py b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_layer_norm.py new file mode 100644 index 00000000000..0da5d7f17c5 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_layer_norm.py @@ -0,0 +1,25 @@ +import torch +import torch.nn.functional as F + +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import ( + CustomModuleMixin, +) + + +class CustomLayerNorm(torch.nn.LayerNorm, CustomModuleMixin): + """Custom wrapper for torch.nn.LayerNorm that supports device autocasting for partial model loading.""" + + def _autocast_forward(self, input: torch.Tensor) -> torch.Tensor: + weight = cast_to_device(self.weight, input.device) if self.weight is not None else None + bias = cast_to_device(self.bias, input.device) if self.bias is not None else None + return F.layer_norm(input, self.normalized_shape, weight, bias, self.eps) + + def forward(self, input: torch.Tensor) -> torch.Tensor: + if len(self._patches_and_weights) > 0: + raise RuntimeError("LayerNorm layers do not support patches") + + if self._device_autocasting_enabled: + return self._autocast_forward(input) + else: + return super().forward(input) diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_linear.py b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_linear.py new file mode 100644 index 00000000000..77227583cd9 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_linear.py @@ -0,0 +1,121 @@ +import copy + +import torch + +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import ( + CustomModuleMixin, +) +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.flux_control_lora_layer import FluxControlLoRALayer +from invokeai.backend.patches.layers.lora_layer import LoRALayer +from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor + + +def linear_lora_forward(input: torch.Tensor, lora_layer: LoRALayer, lora_weight: float) -> torch.Tensor: + """An optimized implementation of the residual calculation for a sidecar linear LoRALayer.""" + # up matrix and down matrix have different ranks so we can't simply multiply them + if lora_layer.up.shape[1] != lora_layer.down.shape[0]: + x = torch.nn.functional.linear(input, lora_layer.get_weight(lora_weight), bias=lora_layer.bias) + x *= lora_weight * lora_layer.scale() + return x + + x = torch.nn.functional.linear(input, lora_layer.down) + if lora_layer.mid is not None: + x = torch.nn.functional.linear(x, lora_layer.mid) + x = torch.nn.functional.linear(x, lora_layer.up, bias=lora_layer.bias) + x *= lora_weight * lora_layer.scale() + return x + + +def autocast_linear_forward_sidecar_patches( + orig_module: torch.nn.Linear, input: torch.Tensor, patches_and_weights: list[tuple[BaseLayerPatch, float]] +) -> torch.Tensor: + """A function that runs a linear layer (quantized or non-quantized) with sidecar patches for a linear layer. + Compatible with both quantized and non-quantized Linear layers. + """ + # First, apply the original linear layer. + # NOTE: We slice the input to match the original weight shape in order to work with FluxControlLoRAs, which + # change the linear layer's in_features. + orig_input = input + input = orig_input[..., : orig_module.in_features] + output = orig_module._autocast_forward(input) + + # Then, apply layers for which we have optimized implementations. + unprocessed_patches_and_weights: list[tuple[BaseLayerPatch, float]] = [] + for patch, patch_weight in patches_and_weights: + # Shallow copy the patch so that we can cast it to the target device without modifying the original patch. + patch = copy.copy(patch) + patch.to(input.device) + + if isinstance(patch, FluxControlLoRALayer): + # Note that we use the original input here, not the sliced input. + output += linear_lora_forward(orig_input, patch, patch_weight) + elif isinstance(patch, LoRALayer): + output += linear_lora_forward(input, patch, patch_weight) + else: + unprocessed_patches_and_weights.append((patch, patch_weight)) + + # Finally, apply any remaining patches. + if len(unprocessed_patches_and_weights) > 0: + weight, bias = orig_module._cast_weight_bias_for_input(input) + # Prepare the original parameters for the patch aggregation. + orig_params = {"weight": weight, "bias": bias} + # Filter out None values. + orig_params = {k: v for k, v in orig_params.items() if v is not None} + + aggregated_param_residuals = orig_module._aggregate_patch_parameters( + unprocessed_patches_and_weights, orig_params=orig_params, device=input.device + ) + residual_weight = orig_module._cast_tensor_for_input(aggregated_param_residuals["weight"], input) + residual_bias = orig_module._cast_tensor_for_input(aggregated_param_residuals.get("bias", None), input) + assert residual_weight is not None + output += torch.nn.functional.linear(input, residual_weight, residual_bias) + + return output + + +class CustomLinear(torch.nn.Linear, CustomModuleMixin): + def _cast_tensor_for_input(self, tensor: torch.Tensor | None, input: torch.Tensor) -> torch.Tensor | None: + tensor = cast_to_device(tensor, input.device) + if ( + tensor is not None + and input.is_floating_point() + and tensor.is_floating_point() + and not isinstance(tensor, GGMLTensor) + and tensor.dtype != input.dtype + ): + tensor = tensor.to(dtype=input.dtype) + return tensor + + def _cast_weight_bias_for_input(self, input: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor | None]: + weight = self._cast_tensor_for_input(self.weight, input) + bias = self._cast_tensor_for_input(self.bias, input) + assert weight is not None + return weight, bias + + def _autocast_forward_with_patches(self, input: torch.Tensor) -> torch.Tensor: + return autocast_linear_forward_sidecar_patches(self, input, self._patches_and_weights) + + def _autocast_forward(self, input: torch.Tensor) -> torch.Tensor: + weight, bias = self._cast_weight_bias_for_input(input) + return torch.nn.functional.linear(input, weight, bias) + + def forward(self, input: torch.Tensor) -> torch.Tensor: + if len(self._patches_and_weights) > 0: + return self._autocast_forward_with_patches(input) + elif self._device_autocasting_enabled: + return self._autocast_forward(input) + elif input.is_floating_point() and ( + (self.weight.is_floating_point() and self.weight.dtype != input.dtype) + or ( + self.bias is not None + and self.bias.is_floating_point() + and not isinstance(self.bias, GGMLTensor) + and self.bias.dtype != input.dtype + ) + ): + weight, bias = self._cast_weight_bias_for_input(input) + return torch.nn.functional.linear(input, weight, bias) + else: + return super().forward(input) diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_module_mixin.py b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_module_mixin.py new file mode 100644 index 00000000000..08ad15c4b6f --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_module_mixin.py @@ -0,0 +1,82 @@ +import copy + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.param_shape_utils import get_param_shape +from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor + + +class CustomModuleMixin: + """A mixin class for custom modules that enables device autocasting of module parameters.""" + + def __init__(self): + self._device_autocasting_enabled = False + self._patches_and_weights: list[tuple[BaseLayerPatch, float]] = [] + + def set_device_autocasting_enabled(self, enabled: bool): + """Pass True to enable autocasting of module parameters to the same device as the input tensor. Pass False to + disable autocasting, which results in slightly faster execution speed when we know that device autocasting is + not needed. + """ + self._device_autocasting_enabled = enabled + + def is_device_autocasting_enabled(self) -> bool: + """Check if device autocasting is enabled for the module.""" + return self._device_autocasting_enabled + + def add_patch(self, patch: BaseLayerPatch, patch_weight: float): + """Add a patch to the module.""" + self._patches_and_weights.append((patch, patch_weight)) + + def clear_patches(self): + """Clear all patches from the module.""" + self._patches_and_weights = [] + + def get_num_patches(self) -> int: + """Get the number of patches in the module.""" + return len(self._patches_and_weights) + + def _aggregate_patch_parameters( + self, + patches_and_weights: list[tuple[BaseLayerPatch, float]], + orig_params: dict[str, torch.Tensor], + device: torch.device | None = None, + ): + """Helper function that aggregates the parameters from all patches into a single dict.""" + # HACK(ryand): If the original parameters are in a quantized format whose weights can't be accessed, we replace + # them with dummy tensors on the 'meta' device. This allows patch layers to access the shapes of the original + # parameters. But, of course, any sub-layers that need to access the actual values of the parameters will fail. + for param_name in orig_params.keys(): + param = orig_params[param_name] + if isinstance(param, torch.nn.Parameter) and type(param.data) is torch.Tensor: + pass + elif type(param) is torch.Tensor: + # Plain tensor (e.g. after cast_to_device moved a Parameter to another device). + pass + elif type(param) is GGMLTensor: + # Move to device and dequantize here. Doing it in the patch layer can result in redundant casts / + # dequantizations. + orig_params[param_name] = param.to(device=device).get_dequantized_tensor() + else: + orig_params[param_name] = torch.empty(get_param_shape(param), device="meta") + + params: dict[str, torch.Tensor] = {} + + for patch, patch_weight in patches_and_weights: + if device is not None: + # Shallow copy the patch so that we can cast it to the target device without modifying the original patch. + patch = copy.copy(patch) + patch.to(device) + + # TODO(ryand): `self` could be a quantized module. Depending on what the patch is doing with the original + # parameters, this might fail or return incorrect results. + layer_params = patch.get_parameters(orig_params, weight=patch_weight) + + for param_name, param_weight in layer_params.items(): + if param_name not in params: + params[param_name] = param_weight + else: + params[param_name] += param_weight + + return params diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/utils.py b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/utils.py new file mode 100644 index 00000000000..60294d9e0c3 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/utils.py @@ -0,0 +1,30 @@ +from typing import overload + +import torch + + +@overload +def add_nullable_tensors(a: None, b: None) -> None: ... + + +@overload +def add_nullable_tensors(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: ... + + +@overload +def add_nullable_tensors(a: torch.Tensor, b: None) -> torch.Tensor: ... + + +@overload +def add_nullable_tensors(a: None, b: torch.Tensor) -> torch.Tensor: ... + + +def add_nullable_tensors(a: torch.Tensor | None, b: torch.Tensor | None) -> torch.Tensor | None: + if a is None and b is None: + return None + elif a is None: + return b + elif b is None: + return a + else: + return a + b diff --git a/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/torch_module_autocast.py b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/torch_module_autocast.py new file mode 100644 index 00000000000..589c33fc305 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/torch_module_autocast.py @@ -0,0 +1,114 @@ +from typing import TypeVar + +import torch +from diffusers.models.normalization import RMSNorm as DiffusersRMSNorm + +from invokeai.backend.flux.modules.layers import RMSNorm as FluxRMSNorm +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_conv1d import ( + CustomConv1d, +) +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_conv2d import ( + CustomConv2d, +) +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_diffusers_rms_norm import ( + CustomDiffusersRMSNorm, +) +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_embedding import ( + CustomEmbedding, +) +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_flux_rms_norm import ( + CustomFluxRMSNorm, +) +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_group_norm import ( + CustomGroupNorm, +) +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_layer_norm import ( + CustomLayerNorm, +) +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_linear import ( + CustomLinear, +) +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_module_mixin import ( + CustomModuleMixin, +) + +AUTOCAST_MODULE_TYPE_MAPPING: dict[type[torch.nn.Module], type[torch.nn.Module]] = { + torch.nn.Linear: CustomLinear, + torch.nn.Conv1d: CustomConv1d, + torch.nn.Conv2d: CustomConv2d, + torch.nn.GroupNorm: CustomGroupNorm, + torch.nn.Embedding: CustomEmbedding, + torch.nn.LayerNorm: CustomLayerNorm, + FluxRMSNorm: CustomFluxRMSNorm, + DiffusersRMSNorm: CustomDiffusersRMSNorm, +} + +try: + # These dependencies are not expected to be present on MacOS. + from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_invoke_linear_8_bit_lt import ( + CustomInvokeLinear8bitLt, + ) + from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.custom_modules.custom_invoke_linear_nf4 import ( + CustomInvokeLinearNF4, + ) + from invokeai.backend.quantization.bnb_llm_int8 import InvokeLinear8bitLt + from invokeai.backend.quantization.bnb_nf4 import InvokeLinearNF4 + + AUTOCAST_MODULE_TYPE_MAPPING[InvokeLinear8bitLt] = CustomInvokeLinear8bitLt + AUTOCAST_MODULE_TYPE_MAPPING[InvokeLinearNF4] = CustomInvokeLinearNF4 +except ImportError: + pass + + +AUTOCAST_MODULE_TYPE_MAPPING_INVERSE = {v: k for k, v in AUTOCAST_MODULE_TYPE_MAPPING.items()} + + +T = TypeVar("T", bound=torch.nn.Module) + + +def wrap_custom_layer(module_to_wrap: torch.nn.Module, custom_layer_type: type[T]) -> T: + # HACK(ryand): We use custom initialization logic so that we can initialize a new custom layer instance from an + # existing layer instance without calling __init__() on the original layer class. We achieve this by copying + # the attributes from the original layer instance to the new instance. + custom_layer = custom_layer_type.__new__(custom_layer_type) + # Note that we share the __dict__. + # TODO(ryand): In the future, we may want to do a shallow copy of the __dict__. + custom_layer.__dict__ = module_to_wrap.__dict__ + + # Initialize the CustomModuleMixin fields. + CustomModuleMixin.__init__(custom_layer) # type: ignore + return custom_layer + + +def unwrap_custom_layer(custom_layer: torch.nn.Module, original_layer_type: type[torch.nn.Module]): + # HACK(ryand): We use custom initialization logic so that we can initialize a new custom layer instance from an + # existing layer instance without calling __init__() on the original layer class. We achieve this by copying + # the attributes from the original layer instance to the new instance. + original_layer = original_layer_type.__new__(original_layer_type) + # Note that we share the __dict__. + # TODO(ryand): In the future, we may want to do a shallow copy of the __dict__ and strip out the CustomModuleMixin + # fields. + original_layer.__dict__ = custom_layer.__dict__ + return original_layer + + +def apply_custom_layers_to_model(module: torch.nn.Module, device_autocasting_enabled: bool = False): + for name, submodule in module.named_children(): + override_type = AUTOCAST_MODULE_TYPE_MAPPING.get(type(submodule), None) + if override_type is not None: + custom_layer = wrap_custom_layer(submodule, override_type) + # TODO(ryand): In the future, we should manage this flag on a per-module basis. + custom_layer.set_device_autocasting_enabled(device_autocasting_enabled) + setattr(module, name, custom_layer) + else: + # Recursively apply to submodules + apply_custom_layers_to_model(submodule, device_autocasting_enabled) + + +def remove_custom_layers_from_model(module: torch.nn.Module): + for name, submodule in module.named_children(): + override_type = AUTOCAST_MODULE_TYPE_MAPPING_INVERSE.get(type(submodule), None) + if override_type is not None: + setattr(module, name, unwrap_custom_layer(submodule, override_type)) + else: + remove_custom_layers_from_model(submodule) diff --git a/invokeai/backend/model_manager/load/model_cache/utils.py b/invokeai/backend/model_manager/load/model_cache/utils.py new file mode 100644 index 00000000000..2b581990c69 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/utils.py @@ -0,0 +1,20 @@ +import itertools + +import torch + + +def get_effective_device(model: torch.nn.Module) -> torch.device: + """A utility to infer the 'effective' device of a model. + + This utility handles the case where a model is partially loaded onto the GPU, so is safer than just calling: + `next(iter(model.parameters())).device`. + + In the worst case, this utility has to check all model parameters, so if you already know the intended model device, + then it is better to avoid calling this function. + """ + # If all parameters are on the CPU, return the CPU device. Otherwise, return the first non-CPU device. + for p in itertools.chain(model.parameters(), model.buffers()): + if p.device.type != "cpu": + return p.device + + return torch.device("cpu") diff --git a/invokeai/backend/model_manager/load/model_loader_registry.py b/invokeai/backend/model_manager/load/model_loader_registry.py new file mode 100644 index 00000000000..ca9ea56edbe --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loader_registry.py @@ -0,0 +1,99 @@ +# Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Development team +""" +This module implements a system in which model loaders register the +type, base and format of models that they know how to load. + +Use like this: + + cls, model_config, submodel_type = ModelLoaderRegistry.get_implementation(model_config, submodel_type) # type: ignore + loaded_model = cls( + app_config=app_config, + logger=logger, + ram_cache=ram_cache, + convert_cache=convert_cache + ).load_model(model_config, submodel_type) + +""" + +from abc import ABC, abstractmethod +from typing import Callable, Dict, Optional, Tuple, Type, TypeVar + +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 import ModelLoaderBase +from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType, SubModelType + + +class ModelLoaderRegistryBase(ABC): + """This class allows model loaders to register their type, base and format.""" + + @classmethod + @abstractmethod + def register( + cls, type: ModelType, format: ModelFormat, base: BaseModelType = BaseModelType.Any + ) -> Callable[[Type[ModelLoaderBase]], Type[ModelLoaderBase]]: + """Define a decorator which registers the subclass of loader.""" + + @classmethod + @abstractmethod + def get_implementation( + cls, config: AnyModelConfig, submodel_type: Optional[SubModelType] + ) -> Tuple[Type[ModelLoaderBase], Config_Base, Optional[SubModelType]]: + """ + Get subclass of ModelLoaderBase registered to handle base and type. + + Parameters: + :param config: Model configuration record, as returned by ModelRecordService + :param submodel_type: Submodel to fetch (main models only) + :return: tuple(loader_class, model_config, submodel_type) + + Note that the returned model config may be different from one what passed + in, in the event that a submodel type is provided. + """ + + +TModelLoader = TypeVar("TModelLoader", bound=ModelLoaderBase) + + +class ModelLoaderRegistry(ModelLoaderRegistryBase): + """ + This class allows model loaders to register their type, base and format. + """ + + _registry: Dict[str, Type[ModelLoaderBase]] = {} + + @classmethod + def register( + cls, type: ModelType, format: ModelFormat, base: BaseModelType = BaseModelType.Any + ) -> Callable[[Type[TModelLoader]], Type[TModelLoader]]: + """Define a decorator which registers the subclass of loader.""" + + def decorator(subclass: Type[TModelLoader]) -> Type[TModelLoader]: + key = cls._to_registry_key(base, type, format) + if key in cls._registry: + raise Exception( + f"{subclass.__name__} is trying to register as a loader for {base}/{type}/{format}, but this type of model has already been registered by {cls._registry[key].__name__}" + ) + cls._registry[key] = subclass + return subclass + + return decorator + + @classmethod + def get_implementation( + cls, config: AnyModelConfig, submodel_type: Optional[SubModelType] + ) -> Tuple[Type[ModelLoaderBase], Config_Base, Optional[SubModelType]]: + """Get subclass of ModelLoaderBase registered to handle base and type.""" + + key1 = cls._to_registry_key(config.base, config.type, config.format) # for a specific base type + key2 = cls._to_registry_key(BaseModelType.Any, config.type, config.format) # with wildcard Any + implementation = cls._registry.get(key1) or cls._registry.get(key2) + if not implementation: + raise NotImplementedError( + f"No subclass of LoadedModel is registered for base={config.base}, type={config.type}, format={config.format}" + ) + return implementation, config, submodel_type + + @staticmethod + def _to_registry_key(base: BaseModelType, type: ModelType, format: ModelFormat) -> str: + return "-".join([base.value, type.value, format.value]) diff --git a/invokeai/backend/model_manager/load/model_loaders/__init__.py b/invokeai/backend/model_manager/load/model_loaders/__init__.py new file mode 100644 index 00000000000..962cba54811 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/__init__.py @@ -0,0 +1,3 @@ +""" +Init file for model_loaders. +""" diff --git a/invokeai/backend/model_manager/load/model_loaders/anima.py b/invokeai/backend/model_manager/load/model_loaders/anima.py new file mode 100644 index 00000000000..8d068f5468c --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/anima.py @@ -0,0 +1,158 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for Anima model loading in InvokeAI.""" + +from pathlib import Path +from typing import Optional + +import accelerate + +from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.configs.main import Main_Checkpoint_Anima_Config +from invokeai.backend.model_manager.load.load_default import ModelLoader +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.taxonomy import ( + AnyModel, + BaseModelType, + ModelFormat, + ModelType, + SubModelType, +) +from invokeai.backend.util.devices import TorchDevice +from invokeai.backend.util.logging import InvokeAILogger + +logger = InvokeAILogger.get_logger(__name__) + + +@ModelLoaderRegistry.register(base=BaseModelType.Anima, type=ModelType.Main, format=ModelFormat.Checkpoint) +class AnimaCheckpointModel(ModelLoader): + """Class to load Anima transformer models from single-file checkpoints. + + The Anima checkpoint contains both the MiniTrainDIT backbone and the LLM Adapter + under a shared `net.` prefix. The loader strips this prefix and instantiates + the AnimaTransformer model with the correct architecture parameters. + """ + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, Checkpoint_Config_Base): + raise ValueError("Only CheckpointConfigBase models are currently supported here.") + + match submodel_type: + case SubModelType.Transformer: + return self._load_from_singlefile(config) + + raise ValueError( + f"Only Transformer submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}" + ) + + def _load_from_singlefile( + self, + config: AnyModelConfig, + ) -> AnyModel: + from safetensors.torch import load_file + + from invokeai.backend.anima.anima_transformer import AnimaTransformer + + if not isinstance(config, Main_Checkpoint_Anima_Config): + raise TypeError( + f"Expected Main_Checkpoint_Anima_Config, got {type(config).__name__}. " + "Model configuration type mismatch." + ) + model_path = Path(config.path) + + # Load the state dict from safetensors + sd = load_file(model_path) + + # Handle different checkpoint packaging formats: + # - Official format: keys prefixed with `net.` (e.g. `net.blocks.0...`) + # - ComfyUI bundled format: transformer keys prefixed with `model.diffusion_model.` + # alongside `first_stage_model.*` (VAE) and `cond_stage_model.*` (text encoder) + prefix_to_strip = None + for prefix in ["model.diffusion_model.", "net."]: + if any(k.startswith(prefix) for k in sd.keys() if isinstance(k, str)): + prefix_to_strip = prefix + break + + if prefix_to_strip: + stripped_sd = {} + for key, value in sd.items(): + if isinstance(key, str) and key.startswith(prefix_to_strip): + stripped_sd[key[len(prefix_to_strip) :]] = value + # Skip non-transformer keys from bundled checkpoints (VAE, text encoder) + sd = stripped_sd + + # Create an empty AnimaTransformer with Anima's default architecture parameters + with accelerate.init_empty_weights(): + model = AnimaTransformer( + max_img_h=240, + max_img_w=240, + max_frames=1, + in_channels=16, + out_channels=16, + patch_spatial=2, + patch_temporal=1, + concat_padding_mask=True, + model_channels=2048, + num_blocks=28, + num_heads=16, + mlp_ratio=4.0, + crossattn_emb_channels=1024, + pos_emb_cls="rope3d", + # Anima reuses the Cosmos-Predict2 2B Text2Image DiT, which trains with + # rope_scale=(t=1.0, h=4.0, w=4.0). The NTK-scaled spatial RoPE base is + # mandatory; omitting it (theta=10000 on all axes) shifts every step's + # velocity ~7% off and compounds into degraded images. Matches diffusers + # CosmosTransformer3DModel rope_scale via *_extrapolation_ratio. + rope_h_extrapolation_ratio=4.0, + rope_w_extrapolation_ratio=4.0, + rope_t_extrapolation_ratio=1.0, + use_adaln_lora=True, + adaln_lora_dim=256, + extra_per_block_abs_pos_emb=False, + image_model="anima", + ) + + # Determine safe dtype + target_device = TorchDevice.choose_torch_device() + model_dtype = TorchDevice.choose_anima_inference_dtype(target_device) + + # Handle memory management + new_sd_size = sum(ten.nelement() * model_dtype.itemsize for ten in sd.values()) + self._ram_cache.make_room(new_sd_size) + + # Convert to target dtype (skip non-float tensors like embedding indices) + for k in sd.keys(): + if sd[k].is_floating_point(): + sd[k] = sd[k].to(model_dtype) + + # Filter out tensors that are regenerated at runtime and therefore not part of the + # in-memory module state. Some community-trained checkpoints (e.g. animaCatTower_v10) + # serialize derived pos_embedder buffers/cached tensors that the official model + # registers as non-persistent (or recomputes locally). + runtime_only_suffixes = ( + ".inv_freq", + "pos_embedder.dim_spatial_range", + "pos_embedder.dim_temporal_range", + "pos_embedder.seq", + ) + keys_to_remove = [k for k in sd.keys() if k.endswith(runtime_only_suffixes)] + for k in keys_to_remove: + del sd[k] + + load_result = model.load_state_dict(sd, assign=True, strict=False) + if load_result.unexpected_keys: + raise RuntimeError( + f"Checkpoint contains {len(load_result.unexpected_keys)} unexpected keys. " + f"This may indicate a corrupted or incompatible checkpoint. " + f"First 5 unexpected keys: {load_result.unexpected_keys[:5]}" + ) + if load_result.missing_keys: + logger.warning( + f"Checkpoint is missing {len(load_result.missing_keys)} keys " + f"(expected for inv_freq buffers). First 5: {load_result.missing_keys[:5]}" + ) + return model diff --git a/invokeai/backend/model_manager/load/model_loaders/clip_vision.py b/invokeai/backend/model_manager/load/model_loaders/clip_vision.py new file mode 100644 index 00000000000..0150e24248f --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/clip_vision.py @@ -0,0 +1,35 @@ +from pathlib import Path +from typing import Optional + +from transformers import CLIPVisionModelWithProjection + +from invokeai.backend.model_manager.configs.base import Diffusers_Config_Base +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.load.load_default import ModelLoader +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.taxonomy import AnyModel, BaseModelType, ModelFormat, ModelType, SubModelType + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.CLIPVision, format=ModelFormat.Diffusers) +class ClipVisionLoader(ModelLoader): + """Class to load CLIPVision models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, Diffusers_Config_Base): + raise ValueError("Only DiffusersConfigBase models are currently supported here.") + + if submodel_type is not None: + raise Exception("There are no submodels in CLIP Vision models.") + + model_path = Path(config.path) + + model = CLIPVisionModelWithProjection.from_pretrained( + model_path, torch_dtype=self._torch_dtype, local_files_only=True + ) + assert isinstance(model, CLIPVisionModelWithProjection) + + return model diff --git a/invokeai/backend/model_manager/load/model_loaders/cogview4.py b/invokeai/backend/model_manager/load/model_loaders/cogview4.py new file mode 100644 index 00000000000..6e8490912bc --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/cogview4.py @@ -0,0 +1,59 @@ +from pathlib import Path +from typing import Optional + +import torch + +from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base, Diffusers_Config_Base +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader +from invokeai.backend.model_manager.taxonomy import ( + AnyModel, + BaseModelType, + ModelFormat, + ModelType, + SubModelType, +) + + +@ModelLoaderRegistry.register(base=BaseModelType.CogView4, type=ModelType.Main, format=ModelFormat.Diffusers) +class CogView4DiffusersModel(GenericDiffusersLoader): + """Class to load CogView4 main models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if isinstance(config, Checkpoint_Config_Base): + raise NotImplementedError("CheckpointConfigBase is not implemented for CogView4 models.") + + if submodel_type is None: + raise Exception("A submodel type must be provided when loading main pipelines.") + + model_path = Path(config.path) + load_class = self.get_hf_load_class(model_path, submodel_type) + repo_variant = config.repo_variant if isinstance(config, Diffusers_Config_Base) else None + variant = repo_variant.value if repo_variant else None + model_path = model_path / submodel_type.value + + # We force bfloat16 for CogView4 models. It produces black images with float16. I haven't tracked down + # specifically which model(s) is/are responsible. + dtype = torch.bfloat16 + try: + result: AnyModel = load_class.from_pretrained( + model_path, + torch_dtype=dtype, + variant=variant, + local_files_only=True, + ) + except OSError as e: + if variant and "no file named" in str( + e + ): # try without the variant, just in case user's preferences changed + result = load_class.from_pretrained(model_path, torch_dtype=dtype, local_files_only=True) + else: + raise e + + result = self._apply_fp8_layerwise_casting(result, config, submodel_type) + return result diff --git a/invokeai/backend/model_manager/load/model_loaders/controlnet.py b/invokeai/backend/model_manager/load/model_loaders/controlnet.py new file mode 100644 index 00000000000..e50e45849ab --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/controlnet.py @@ -0,0 +1,55 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for ControlNet model loading in InvokeAI.""" + +from typing import Optional + +from diffusers import ControlNetModel + +from invokeai.backend.model_manager.configs.controlnet import ControlNet_Checkpoint_Config_Base +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader +from invokeai.backend.model_manager.taxonomy import ( + AnyModel, + BaseModelType, + ModelFormat, + ModelType, + SubModelType, +) + + +@ModelLoaderRegistry.register( + base=BaseModelType.StableDiffusion1, type=ModelType.ControlNet, format=ModelFormat.Diffusers +) +@ModelLoaderRegistry.register( + base=BaseModelType.StableDiffusion1, type=ModelType.ControlNet, format=ModelFormat.Checkpoint +) +@ModelLoaderRegistry.register( + base=BaseModelType.StableDiffusion2, type=ModelType.ControlNet, format=ModelFormat.Diffusers +) +@ModelLoaderRegistry.register( + base=BaseModelType.StableDiffusion2, type=ModelType.ControlNet, format=ModelFormat.Checkpoint +) +@ModelLoaderRegistry.register( + base=BaseModelType.StableDiffusionXL, type=ModelType.ControlNet, format=ModelFormat.Diffusers +) +@ModelLoaderRegistry.register( + base=BaseModelType.StableDiffusionXL, type=ModelType.ControlNet, format=ModelFormat.Checkpoint +) +class ControlNetLoader(GenericDiffusersLoader): + """Class to load ControlNet models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if isinstance(config, ControlNet_Checkpoint_Config_Base): + result = ControlNetModel.from_single_file( + config.path, + torch_dtype=self._torch_dtype, + ) + result = self._apply_fp8_layerwise_casting(result, config, submodel_type) + return result + else: + return super()._load_model(config, submodel_type) diff --git a/invokeai/backend/model_manager/load/model_loaders/flux.py b/invokeai/backend/model_manager/load/model_loaders/flux.py new file mode 100644 index 00000000000..b3c46d04db3 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/flux.py @@ -0,0 +1,1477 @@ +# Copyright (c) 2024, Brandon W. Rising and the InvokeAI Development Team +"""Class for Flux model loading in InvokeAI.""" + +from pathlib import Path +from typing import Optional + +import accelerate +import torch +from safetensors.torch import load_file +from transformers import ( + AutoConfig, + AutoModelForTextEncoding, + CLIPTextModel, + CLIPTokenizer, + T5EncoderModel, + T5TokenizerFast, +) + +from invokeai.app.services.config.config_default import get_config +from invokeai.backend.flux.controlnet.instantx_controlnet_flux import InstantXControlNetFlux +from invokeai.backend.flux.controlnet.state_dict_utils import ( + convert_diffusers_instantx_state_dict_to_bfl_format, + infer_flux_params_from_state_dict, + infer_instantx_num_control_modes_from_state_dict, + is_state_dict_instantx_controlnet, + is_state_dict_xlabs_controlnet, +) +from invokeai.backend.flux.controlnet.xlabs_controlnet_flux import XLabsControlNetFlux +from invokeai.backend.flux.ip_adapter.state_dict_utils import infer_xlabs_ip_adapter_params_from_state_dict +from invokeai.backend.flux.ip_adapter.xlabs_ip_adapter_flux import ( + XlabsIpAdapterFlux, +) +from invokeai.backend.flux.model import Flux +from invokeai.backend.flux.modules.autoencoder import AutoEncoder +from invokeai.backend.flux.redux.flux_redux_model import FluxReduxModel +from invokeai.backend.flux.util import get_flux_ae_params, get_flux_transformers_params +from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base, Diffusers_Config_Base +from invokeai.backend.model_manager.configs.clip_embed import CLIPEmbed_Diffusers_Config_Base +from invokeai.backend.model_manager.configs.controlnet import ( + ControlNet_Checkpoint_Config_Base, + ControlNet_Diffusers_Config_Base, +) +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.configs.flux_redux import FLUXRedux_Checkpoint_Config +from invokeai.backend.model_manager.configs.ip_adapter import IPAdapter_Checkpoint_Config_Base +from invokeai.backend.model_manager.configs.main import ( + Main_BnBNF4_FLUX_Config, + Main_Checkpoint_Flux2_Config, + Main_Checkpoint_FLUX_Config, + Main_GGUF_Flux2_Config, + Main_GGUF_FLUX_Config, +) +from invokeai.backend.model_manager.configs.t5_encoder import T5Encoder_BnBLLMint8_Config, T5Encoder_T5Encoder_Config +from invokeai.backend.model_manager.configs.vae import VAE_Checkpoint_Config_Base, VAE_Checkpoint_Flux2_Config +from invokeai.backend.model_manager.load.load_default import ModelLoader +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader +from invokeai.backend.model_manager.taxonomy import ( + AnyModel, + BaseModelType, + FluxVariantType, + ModelFormat, + ModelType, + SubModelType, +) +from invokeai.backend.model_manager.util.model_util import ( + convert_bundle_to_flux_transformer_checkpoint, +) +from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader +from invokeai.backend.quantization.gguf.utils import TORCH_COMPATIBLE_QTYPES +from invokeai.backend.util.silence_warnings import SilenceWarnings + +try: + from invokeai.backend.quantization.bnb_llm_int8 import quantize_model_llm_int8 + from invokeai.backend.quantization.bnb_nf4 import quantize_model_nf4 + + bnb_available = True +except ImportError: + bnb_available = False + +app_config = get_config() + + +@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.VAE, format=ModelFormat.Checkpoint) +class FluxVAELoader(ModelLoader): + """Class to load VAE models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, VAE_Checkpoint_Config_Base): + raise ValueError("Only VAECheckpointConfig models are currently supported here.") + model_path = Path(config.path) + + with accelerate.init_empty_weights(): + model = AutoEncoder(get_flux_ae_params()) + sd = load_file(model_path) + model.load_state_dict(sd, assign=True) + # VAE is broken in float16, which mps defaults to + if self._torch_dtype == torch.float16: + try: + vae_dtype = torch.tensor([1.0], dtype=torch.bfloat16, device=self._torch_device).dtype + except TypeError: + vae_dtype = torch.float32 + else: + vae_dtype = self._torch_dtype + model.to(vae_dtype) + + return model + + +@ModelLoaderRegistry.register(base=BaseModelType.Flux2, type=ModelType.VAE, format=ModelFormat.Diffusers) +class Flux2VAEDiffusersLoader(ModelLoader): + """Class to load FLUX.2 VAE models in diffusers format (AutoencoderKLFlux2 with 32 latent channels).""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + from diffusers import AutoencoderKLFlux2 + + model_path = Path(config.path) + + # VAE is broken in float16, which mps defaults to + if self._torch_dtype == torch.float16: + try: + vae_dtype = torch.tensor([1.0], dtype=torch.bfloat16, device=self._torch_device).dtype + except TypeError: + vae_dtype = torch.float32 + else: + vae_dtype = self._torch_dtype + + model = AutoencoderKLFlux2.from_pretrained( + model_path, + torch_dtype=vae_dtype, + local_files_only=True, + ) + + model = self._apply_fp8_layerwise_casting(model, config, submodel_type) + return model + + +@ModelLoaderRegistry.register(base=BaseModelType.Flux2, type=ModelType.VAE, format=ModelFormat.Checkpoint) +class Flux2VAELoader(ModelLoader): + """Class to load FLUX.2 VAE models (AutoencoderKLFlux2 with 32 latent channels).""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, VAE_Checkpoint_Flux2_Config): + raise ValueError("Only VAE_Checkpoint_Flux2_Config models are currently supported here.") + + from diffusers import AutoencoderKLFlux2 + + model_path = Path(config.path) + + # Load state dict manually since from_single_file may not support AutoencoderKLFlux2 yet + sd = load_file(model_path) + + # Convert BFL format to diffusers format if needed + # BFL format uses: encoder.down., decoder.up., decoder.mid.block_1, decoder.mid.attn_1, decoder.norm_out + # Diffusers uses: encoder.down_blocks., decoder.up_blocks., decoder.mid_block.resnets., decoder.conv_norm_out + is_bfl_format = any( + k.startswith("encoder.down.") + or k.startswith("decoder.up.") + or k.startswith("decoder.mid.block_") + or k.startswith("decoder.mid.attn_") + or k.startswith("decoder.norm_out") + or k.startswith("encoder.mid.block_") + or k.startswith("encoder.mid.attn_") + or k.startswith("encoder.norm_out") + for k in sd.keys() + ) + if is_bfl_format: + sd = self._convert_flux2_vae_bfl_to_diffusers(sd) + + # FLUX.2 VAE configuration (32 latent channels). + # The standard FLUX.2 VAE uses block_out_channels=(128,256,512,512) for both + # encoder and decoder. The "small decoder" variant from + # black-forest-labs/FLUX.2-small-decoder keeps the full encoder but uses a + # narrower decoder with channels (96,192,384,384). AutoencoderKLFlux2 only + # exposes a single block_out_channels, so we build the model with the + # encoder's channels and, if the decoder differs, replace just the decoder + # submodule with a matching one before loading the state dict. + encoder_block_out_channels = (128, 256, 512, 512) + decoder_block_out_channels = encoder_block_out_channels + if "encoder.conv_in.weight" in sd and "encoder.conv_norm_out.weight" in sd: + enc_last = int(sd["encoder.conv_norm_out.weight"].shape[0]) + enc_first = int(sd["encoder.conv_in.weight"].shape[0]) + encoder_block_out_channels = (enc_first, enc_first * 2, enc_last, enc_last) + if "decoder.conv_in.weight" in sd and "decoder.conv_norm_out.weight" in sd: + dec_last = int(sd["decoder.conv_in.weight"].shape[0]) + dec_first = int(sd["decoder.conv_norm_out.weight"].shape[0]) + decoder_block_out_channels = (dec_first, dec_first * 2, dec_last, dec_last) + + with SilenceWarnings(): + with accelerate.init_empty_weights(): + model = AutoencoderKLFlux2(block_out_channels=encoder_block_out_channels) + if decoder_block_out_channels != encoder_block_out_channels: + # Rebuild the decoder with the smaller channel widths. + from diffusers.models.autoencoders.vae import Decoder + + cfg = model.config + model.decoder = Decoder( + in_channels=cfg.latent_channels, + out_channels=cfg.out_channels, + up_block_types=cfg.up_block_types, + block_out_channels=decoder_block_out_channels, + layers_per_block=cfg.layers_per_block, + norm_num_groups=cfg.norm_num_groups, + act_fn=cfg.act_fn, + mid_block_add_attention=cfg.mid_block_add_attention, + ) + + # Convert to bfloat16 and load + for k in sd.keys(): + sd[k] = sd[k].to(torch.bfloat16) + + model.load_state_dict(sd, assign=True) + + # VAE is broken in float16, which mps defaults to + if self._torch_dtype == torch.float16: + try: + vae_dtype = torch.tensor([1.0], dtype=torch.bfloat16, device=self._torch_device).dtype + except TypeError: + vae_dtype = torch.float32 + else: + vae_dtype = self._torch_dtype + model.to(vae_dtype) + + model = self._apply_fp8_layerwise_casting(model, config, submodel_type) + return model + + def _convert_flux2_vae_bfl_to_diffusers(self, sd: dict) -> dict: + """Convert FLUX.2 VAE BFL format state dict to diffusers format. + + Key differences: + - encoder.down.X.block.Y -> encoder.down_blocks.X.resnets.Y + - encoder.down.X.downsample.conv -> encoder.down_blocks.X.downsamplers.0.conv + - encoder.mid.block_1/2 -> encoder.mid_block.resnets.0/1 + - encoder.mid.attn_1.q/k/v -> encoder.mid_block.attentions.0.to_q/k/v + - encoder.norm_out -> encoder.conv_norm_out + - encoder.quant_conv -> quant_conv (top-level) + - decoder.up.X -> decoder.up_blocks.(num_blocks-1-X) (reversed order!) + - decoder.post_quant_conv -> post_quant_conv (top-level) + - *.nin_shortcut -> *.conv_shortcut + """ + import re + + converted = {} + num_up_blocks = 4 # Standard VAE has 4 up blocks + + for old_key, tensor in sd.items(): + new_key = old_key + + # Encoder down blocks: encoder.down.X.block.Y -> encoder.down_blocks.X.resnets.Y + match = re.match(r"encoder\.down\.(\d+)\.block\.(\d+)\.(.*)", old_key) + if match: + block_idx, resnet_idx, rest = match.groups() + rest = rest.replace("nin_shortcut", "conv_shortcut") + new_key = f"encoder.down_blocks.{block_idx}.resnets.{resnet_idx}.{rest}" + converted[new_key] = tensor + continue + + # Encoder downsamplers: encoder.down.X.downsample.conv -> encoder.down_blocks.X.downsamplers.0.conv + match = re.match(r"encoder\.down\.(\d+)\.downsample\.conv\.(.*)", old_key) + if match: + block_idx, rest = match.groups() + new_key = f"encoder.down_blocks.{block_idx}.downsamplers.0.conv.{rest}" + converted[new_key] = tensor + continue + + # Encoder mid block resnets: encoder.mid.block_1/2 -> encoder.mid_block.resnets.0/1 + match = re.match(r"encoder\.mid\.block_(\d+)\.(.*)", old_key) + if match: + block_num, rest = match.groups() + resnet_idx = int(block_num) - 1 # block_1 -> resnets.0, block_2 -> resnets.1 + new_key = f"encoder.mid_block.resnets.{resnet_idx}.{rest}" + converted[new_key] = tensor + continue + + # Encoder mid block attention: encoder.mid.attn_1.* -> encoder.mid_block.attentions.0.* + match = re.match(r"encoder\.mid\.attn_1\.(.*)", old_key) + if match: + rest = match.group(1) + # Map attention keys + # BFL uses Conv2d (shape [out, in, 1, 1]), diffusers uses Linear (shape [out, in]) + # Squeeze the extra dimensions for weight tensors + if rest.startswith("q."): + new_key = f"encoder.mid_block.attentions.0.to_q.{rest[2:]}" + if rest.endswith(".weight") and tensor.dim() == 4: + tensor = tensor.squeeze(-1).squeeze(-1) + elif rest.startswith("k."): + new_key = f"encoder.mid_block.attentions.0.to_k.{rest[2:]}" + if rest.endswith(".weight") and tensor.dim() == 4: + tensor = tensor.squeeze(-1).squeeze(-1) + elif rest.startswith("v."): + new_key = f"encoder.mid_block.attentions.0.to_v.{rest[2:]}" + if rest.endswith(".weight") and tensor.dim() == 4: + tensor = tensor.squeeze(-1).squeeze(-1) + elif rest.startswith("proj_out."): + new_key = f"encoder.mid_block.attentions.0.to_out.0.{rest[9:]}" + if rest.endswith(".weight") and tensor.dim() == 4: + tensor = tensor.squeeze(-1).squeeze(-1) + elif rest.startswith("norm."): + new_key = f"encoder.mid_block.attentions.0.group_norm.{rest[5:]}" + else: + new_key = f"encoder.mid_block.attentions.0.{rest}" + converted[new_key] = tensor + continue + + # Encoder norm_out -> conv_norm_out + if old_key.startswith("encoder.norm_out."): + new_key = old_key.replace("encoder.norm_out.", "encoder.conv_norm_out.") + converted[new_key] = tensor + continue + + # Encoder quant_conv -> quant_conv (move to top level) + if old_key.startswith("encoder.quant_conv."): + new_key = old_key.replace("encoder.quant_conv.", "quant_conv.") + converted[new_key] = tensor + continue + + # Decoder up blocks (reversed order!): decoder.up.X -> decoder.up_blocks.(num_blocks-1-X) + match = re.match(r"decoder\.up\.(\d+)\.block\.(\d+)\.(.*)", old_key) + if match: + block_idx, resnet_idx, rest = match.groups() + # Reverse the block index + new_block_idx = num_up_blocks - 1 - int(block_idx) + rest = rest.replace("nin_shortcut", "conv_shortcut") + new_key = f"decoder.up_blocks.{new_block_idx}.resnets.{resnet_idx}.{rest}" + converted[new_key] = tensor + continue + + # Decoder upsamplers (reversed order!) + match = re.match(r"decoder\.up\.(\d+)\.upsample\.conv\.(.*)", old_key) + if match: + block_idx, rest = match.groups() + new_block_idx = num_up_blocks - 1 - int(block_idx) + new_key = f"decoder.up_blocks.{new_block_idx}.upsamplers.0.conv.{rest}" + converted[new_key] = tensor + continue + + # Decoder mid block resnets: decoder.mid.block_1/2 -> decoder.mid_block.resnets.0/1 + match = re.match(r"decoder\.mid\.block_(\d+)\.(.*)", old_key) + if match: + block_num, rest = match.groups() + resnet_idx = int(block_num) - 1 + new_key = f"decoder.mid_block.resnets.{resnet_idx}.{rest}" + converted[new_key] = tensor + continue + + # Decoder mid block attention: decoder.mid.attn_1.* -> decoder.mid_block.attentions.0.* + match = re.match(r"decoder\.mid\.attn_1\.(.*)", old_key) + if match: + rest = match.group(1) + # BFL uses Conv2d (shape [out, in, 1, 1]), diffusers uses Linear (shape [out, in]) + # Squeeze the extra dimensions for weight tensors + if rest.startswith("q."): + new_key = f"decoder.mid_block.attentions.0.to_q.{rest[2:]}" + if rest.endswith(".weight") and tensor.dim() == 4: + tensor = tensor.squeeze(-1).squeeze(-1) + elif rest.startswith("k."): + new_key = f"decoder.mid_block.attentions.0.to_k.{rest[2:]}" + if rest.endswith(".weight") and tensor.dim() == 4: + tensor = tensor.squeeze(-1).squeeze(-1) + elif rest.startswith("v."): + new_key = f"decoder.mid_block.attentions.0.to_v.{rest[2:]}" + if rest.endswith(".weight") and tensor.dim() == 4: + tensor = tensor.squeeze(-1).squeeze(-1) + elif rest.startswith("proj_out."): + new_key = f"decoder.mid_block.attentions.0.to_out.0.{rest[9:]}" + if rest.endswith(".weight") and tensor.dim() == 4: + tensor = tensor.squeeze(-1).squeeze(-1) + elif rest.startswith("norm."): + new_key = f"decoder.mid_block.attentions.0.group_norm.{rest[5:]}" + else: + new_key = f"decoder.mid_block.attentions.0.{rest}" + converted[new_key] = tensor + continue + + # Decoder norm_out -> conv_norm_out + if old_key.startswith("decoder.norm_out."): + new_key = old_key.replace("decoder.norm_out.", "decoder.conv_norm_out.") + converted[new_key] = tensor + continue + + # Decoder post_quant_conv -> post_quant_conv (move to top level) + if old_key.startswith("decoder.post_quant_conv."): + new_key = old_key.replace("decoder.post_quant_conv.", "post_quant_conv.") + converted[new_key] = tensor + continue + + # Keep other keys as-is (like encoder.conv_in, decoder.conv_in, decoder.conv_out, bn.*) + converted[new_key] = tensor + + return converted + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.CLIPEmbed, format=ModelFormat.Diffusers) +class CLIPDiffusersLoader(ModelLoader): + """Class to load main models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, CLIPEmbed_Diffusers_Config_Base): + raise ValueError("Only CLIPEmbedDiffusersConfig models are currently supported here.") + + match submodel_type: + case SubModelType.Tokenizer: + return CLIPTokenizer.from_pretrained(Path(config.path) / "tokenizer", local_files_only=True) + case SubModelType.TextEncoder: + return CLIPTextModel.from_pretrained(Path(config.path) / "text_encoder", local_files_only=True) + + raise ValueError( + f"Only Tokenizer and TextEncoder submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}" + ) + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.T5Encoder, format=ModelFormat.BnbQuantizedLlmInt8b) +class BnbQuantizedLlmInt8bCheckpointModel(ModelLoader): + """Class to load main models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, T5Encoder_BnBLLMint8_Config): + raise ValueError("Only T5EncoderBnbQuantizedLlmInt8bConfig models are currently supported here.") + if not bnb_available: + raise ImportError( + "The bnb modules are not available. Please install bitsandbytes if available on your platform." + ) + match submodel_type: + case SubModelType.Tokenizer2 | SubModelType.Tokenizer3: + return T5TokenizerFast.from_pretrained( + Path(config.path) / "tokenizer_2", max_length=512, local_files_only=True + ) + case SubModelType.TextEncoder2 | SubModelType.TextEncoder3: + te2_model_path = Path(config.path) / "text_encoder_2" + model_config = AutoConfig.from_pretrained(te2_model_path, local_files_only=True) + with accelerate.init_empty_weights(): + model = AutoModelForTextEncoding.from_config(model_config) + model = quantize_model_llm_int8(model, modules_to_not_convert=set()) + + state_dict_path = te2_model_path / "bnb_llm_int8_model.safetensors" + state_dict = load_file(state_dict_path) + self._load_state_dict_into_t5(model, state_dict) + + return model + + raise ValueError( + f"Only Tokenizer and TextEncoder submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}" + ) + + @classmethod + def _load_state_dict_into_t5(cls, model: T5EncoderModel, state_dict: dict[str, torch.Tensor]): + # There is a shared reference to a single weight tensor in the model. + # Both "encoder.embed_tokens.weight" and "shared.weight" refer to the same tensor, so only the latter should + # be present in the state_dict. + missing_keys, unexpected_keys = model.load_state_dict(state_dict, strict=False, assign=True) + assert len(unexpected_keys) == 0 + assert set(missing_keys) == {"encoder.embed_tokens.weight"} + # Assert that the layers we expect to be shared are actually shared. + assert model.encoder.embed_tokens.weight is model.shared.weight + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.T5Encoder, format=ModelFormat.T5Encoder) +class T5EncoderCheckpointModel(ModelLoader): + """Class to load main models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, T5Encoder_T5Encoder_Config): + raise ValueError("Only T5EncoderConfig models are currently supported here.") + + match submodel_type: + case SubModelType.Tokenizer2 | SubModelType.Tokenizer3: + return T5TokenizerFast.from_pretrained( + Path(config.path) / "tokenizer_2", max_length=512, local_files_only=True + ) + case SubModelType.TextEncoder2 | SubModelType.TextEncoder3: + return T5EncoderModel.from_pretrained( + Path(config.path) / "text_encoder_2", + torch_dtype="auto", + low_cpu_mem_usage=True, + local_files_only=True, + ) + + raise ValueError( + f"Only Tokenizer and TextEncoder submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}" + ) + + +@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.Main, format=ModelFormat.Checkpoint) +class FluxCheckpointModel(ModelLoader): + """Class to load main models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, Checkpoint_Config_Base): + raise ValueError("Only CheckpointConfigBase models are currently supported here.") + + match submodel_type: + case SubModelType.Transformer: + model = self._load_from_singlefile(config) + model = self._apply_fp8_layerwise_casting(model, config, submodel_type) + return model + + raise ValueError( + f"Only Transformer submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}" + ) + + def _load_from_singlefile( + self, + config: AnyModelConfig, + ) -> AnyModel: + assert isinstance(config, Main_Checkpoint_FLUX_Config) + model_path = Path(config.path) + + with accelerate.init_empty_weights(): + model = Flux(get_flux_transformers_params(config.variant)) + + sd = load_file(model_path) + if "model.diffusion_model.double_blocks.0.img_attn.norm.key_norm.scale" in sd: + sd = convert_bundle_to_flux_transformer_checkpoint(sd) + new_sd_size = sum([ten.nelement() * torch.bfloat16.itemsize for ten in sd.values()]) + self._ram_cache.make_room(new_sd_size) + for k in sd.keys(): + # We need to cast to bfloat16 due to it being the only currently supported dtype for inference + sd[k] = sd[k].to(torch.bfloat16) + model.load_state_dict(sd, assign=True) + return model + + +@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.Main, format=ModelFormat.GGUFQuantized) +class FluxGGUFCheckpointModel(ModelLoader): + """Class to load GGUF main models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, Checkpoint_Config_Base): + raise ValueError("Only CheckpointConfigBase models are currently supported here.") + + match submodel_type: + case SubModelType.Transformer: + return self._load_from_singlefile(config) + + raise ValueError( + f"Only Transformer submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}" + ) + + def _load_from_singlefile( + self, + config: AnyModelConfig, + ) -> AnyModel: + assert isinstance(config, Main_GGUF_FLUX_Config) + model_path = Path(config.path) + + with accelerate.init_empty_weights(): + model = Flux(get_flux_transformers_params(config.variant)) + + # HACK(ryand): We shouldn't be hard-coding the compute_dtype here. + sd = gguf_sd_loader(model_path, compute_dtype=torch.bfloat16) + + # HACK(ryand): There are some broken GGUF models in circulation that have the wrong shape for img_in.weight. + # We override the shape here to fix the issue. + # Example model with this issue (Q4_K_M): https://civitai.com/models/705823/ggufk-flux-unchained-km-quants + img_in_weight = sd.get("img_in.weight", None) + if img_in_weight is not None and img_in_weight._ggml_quantization_type in TORCH_COMPATIBLE_QTYPES: + expected_img_in_weight_shape = model.img_in.weight.shape + img_in_weight.quantized_data = img_in_weight.quantized_data.view(expected_img_in_weight_shape) + img_in_weight.tensor_shape = expected_img_in_weight_shape + + model.load_state_dict(sd, assign=True) + return model + + +@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.Main, format=ModelFormat.BnbQuantizednf4b) +class FluxBnbQuantizednf4bCheckpointModel(ModelLoader): + """Class to load main models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, Checkpoint_Config_Base): + raise ValueError("Only CheckpointConfigBase models are currently supported here.") + + match submodel_type: + case SubModelType.Transformer: + return self._load_from_singlefile(config) + + raise ValueError( + f"Only Transformer submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}" + ) + + def _load_from_singlefile( + self, + config: AnyModelConfig, + ) -> AnyModel: + assert isinstance(config, Main_BnBNF4_FLUX_Config) + if not bnb_available: + raise ImportError( + "The bnb modules are not available. Please install bitsandbytes if available on your platform." + ) + model_path = Path(config.path) + + with SilenceWarnings(): + with accelerate.init_empty_weights(): + model = Flux(get_flux_transformers_params(config.variant)) + model = quantize_model_nf4(model, modules_to_not_convert=set(), compute_dtype=torch.bfloat16) + sd = load_file(model_path) + if "model.diffusion_model.double_blocks.0.img_attn.norm.key_norm.scale" in sd: + sd = convert_bundle_to_flux_transformer_checkpoint(sd) + model.load_state_dict(sd, assign=True) + return model + + +@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.Main, format=ModelFormat.Diffusers) +class FluxDiffusersModel(GenericDiffusersLoader): + """Class to load FLUX.1 main models in diffusers format.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if isinstance(config, Checkpoint_Config_Base): + raise NotImplementedError("CheckpointConfigBase is not implemented for FLUX diffusers models.") + + if submodel_type is None: + raise Exception("A submodel type must be provided when loading main pipelines.") + + model_path = Path(config.path) + load_class = self.get_hf_load_class(model_path, submodel_type) + repo_variant = config.repo_variant if isinstance(config, Diffusers_Config_Base) else None + variant = repo_variant.value if repo_variant else None + model_path = model_path / submodel_type.value + + # We force bfloat16 for FLUX models. This is required for correct inference. + dtype = torch.bfloat16 + try: + result: AnyModel = load_class.from_pretrained( + model_path, + torch_dtype=dtype, + variant=variant, + local_files_only=True, + ) + except OSError as e: + if variant and "no file named" in str( + e + ): # try without the variant, just in case user's preferences changed + result = load_class.from_pretrained(model_path, torch_dtype=dtype, local_files_only=True) + else: + raise e + + result = self._apply_fp8_layerwise_casting(result, config, submodel_type) + return result + + +@ModelLoaderRegistry.register(base=BaseModelType.Flux2, type=ModelType.Main, format=ModelFormat.Diffusers) +class Flux2DiffusersModel(GenericDiffusersLoader): + """Class to load FLUX.2 main models in diffusers format (e.g. FLUX.2 Klein).""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if isinstance(config, Checkpoint_Config_Base): + raise NotImplementedError("CheckpointConfigBase is not implemented for FLUX.2 diffusers models.") + + if submodel_type is None: + raise Exception("A submodel type must be provided when loading main pipelines.") + + model_path = Path(config.path) + load_class = self.get_hf_load_class(model_path, submodel_type) + repo_variant = config.repo_variant if isinstance(config, Diffusers_Config_Base) else None + variant = repo_variant.value if repo_variant else None + model_path = model_path / submodel_type.value + + # We force bfloat16 for FLUX.2 models. This is required for correct inference. + # We use low_cpu_mem_usage=False to avoid meta tensors for weights not in checkpoint. + # FLUX.2 Klein models may have guidance_embeds=False, so the guidance_embed layers + # won't be in the checkpoint but the model class still creates them. + # We use SilenceWarnings to suppress the "guidance_embeds is not expected" warning + # from diffusers Flux2Transformer2DModel. + dtype = torch.bfloat16 + with SilenceWarnings(): + try: + result: AnyModel = load_class.from_pretrained( + model_path, + torch_dtype=dtype, + variant=variant, + local_files_only=True, + low_cpu_mem_usage=False, + ) + except OSError as e: + if variant and "no file named" in str( + e + ): # try without the variant, just in case user's preferences changed + result = load_class.from_pretrained( + model_path, + torch_dtype=dtype, + local_files_only=True, + low_cpu_mem_usage=False, + ) + else: + raise e + + # For Klein models without guidance_embeds, zero out the guidance_embedder weights + # that were randomly initialized by diffusers. This prevents noise from affecting + # the time embeddings. + if submodel_type == SubModelType.Transformer and hasattr(result, "time_guidance_embed"): + # Check if this is a Klein model without guidance (guidance_embeds=False in config) + transformer_config_path = model_path / "config.json" + if transformer_config_path.exists(): + import json + + with open(transformer_config_path, "r") as f: + transformer_config = json.load(f) + if not transformer_config.get("guidance_embeds", True): + # Zero out the guidance embedder weights + guidance_emb = result.time_guidance_embed.guidance_embedder + if hasattr(guidance_emb, "linear_1"): + guidance_emb.linear_1.weight.data.zero_() + if guidance_emb.linear_1.bias is not None: + guidance_emb.linear_1.bias.data.zero_() + if hasattr(guidance_emb, "linear_2"): + guidance_emb.linear_2.weight.data.zero_() + if guidance_emb.linear_2.bias is not None: + guidance_emb.linear_2.bias.data.zero_() + + result = self._apply_fp8_layerwise_casting(result, config, submodel_type) + return result + + +@ModelLoaderRegistry.register(base=BaseModelType.Flux2, type=ModelType.Main, format=ModelFormat.Checkpoint) +class Flux2CheckpointModel(ModelLoader): + """Class to load FLUX.2 transformer models from single-file checkpoints (safetensors).""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, Checkpoint_Config_Base): + raise ValueError("Only CheckpointConfigBase models are currently supported here.") + + match submodel_type: + case SubModelType.Transformer: + model = self._load_from_singlefile(config) + model = self._apply_fp8_layerwise_casting(model, config, submodel_type) + return model + + raise ValueError( + f"Only Transformer submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}" + ) + + def _load_from_singlefile( + self, + config: AnyModelConfig, + ) -> AnyModel: + from diffusers import Flux2Transformer2DModel + + if not isinstance(config, Main_Checkpoint_Flux2_Config): + raise TypeError( + f"Expected Main_Checkpoint_Flux2_Config, got {type(config).__name__}. " + "Model configuration type mismatch." + ) + model_path = Path(config.path) + + # Load state dict + sd = load_file(model_path) + + # Handle FP8 quantized weights (ComfyUI-style or scaled FP8) + # These store weights as: layer.weight (FP8) + layer.weight_scale (FP32 scalar) + sd = self._dequantize_fp8_weights(sd) + + # Check if keys have ComfyUI-style prefix and strip if needed + prefix_to_strip = None + for prefix in ["model.diffusion_model.", "diffusion_model."]: + if any(k.startswith(prefix) for k in sd.keys() if isinstance(k, str)): + prefix_to_strip = prefix + break + + if prefix_to_strip: + sd = { + (k[len(prefix_to_strip) :] if isinstance(k, str) and k.startswith(prefix_to_strip) else k): v + for k, v in sd.items() + } + + # Convert BFL format state dict to diffusers format + converted_sd = self._convert_flux2_bfl_to_diffusers(sd) + + # Detect architecture from checkpoint keys + double_block_indices = [ + int(k.split(".")[1]) + for k in converted_sd.keys() + if isinstance(k, str) and k.startswith("transformer_blocks.") + ] + single_block_indices = [ + int(k.split(".")[1]) + for k in converted_sd.keys() + if isinstance(k, str) and k.startswith("single_transformer_blocks.") + ] + + num_layers = max(double_block_indices) + 1 if double_block_indices else 5 + num_single_layers = max(single_block_indices) + 1 if single_block_indices else 20 + + # Get dimensions from weights + # context_embedder.weight shape: [hidden_size, joint_attention_dim] + context_embedder_weight = converted_sd.get("context_embedder.weight") + if context_embedder_weight is not None: + hidden_size = context_embedder_weight.shape[0] + joint_attention_dim = context_embedder_weight.shape[1] + else: + # Default to Klein 4B dimensions + hidden_size = 3072 + joint_attention_dim = 7680 + + x_embedder_weight = converted_sd.get("x_embedder.weight") + if x_embedder_weight is not None: + in_channels = x_embedder_weight.shape[1] + else: + in_channels = 128 + + # Calculate num_attention_heads from hidden_size + # Klein 4B: hidden_size=3072, num_attention_heads=24 (3072/128=24) + # Klein 9B: hidden_size=4096, num_attention_heads=32 (4096/128=32) + attention_head_dim = 128 + num_attention_heads = hidden_size // attention_head_dim + + # Klein models don't have guidance embeddings - check if they're in the checkpoint + has_guidance = "time_guidance_embed.guidance_embedder.linear_1.weight" in converted_sd + + # Create model with detected configuration + with SilenceWarnings(): + with accelerate.init_empty_weights(): + model = Flux2Transformer2DModel( + in_channels=in_channels, + out_channels=in_channels, + num_layers=num_layers, + num_single_layers=num_single_layers, + attention_head_dim=attention_head_dim, + num_attention_heads=num_attention_heads, + joint_attention_dim=joint_attention_dim, + patch_size=1, + ) + + # If Klein model without guidance, initialize guidance embedder with zeros + if not has_guidance: + # Get the expected dimensions from timestep embedder (they should match) + timestep_linear1 = converted_sd.get("time_guidance_embed.timestep_embedder.linear_1.weight") + if timestep_linear1 is not None: + in_features = timestep_linear1.shape[1] + out_features = timestep_linear1.shape[0] + # Initialize guidance embedder with same shape as timestep embedder + converted_sd["time_guidance_embed.guidance_embedder.linear_1.weight"] = torch.zeros( + out_features, in_features, dtype=torch.bfloat16 + ) + timestep_linear2 = converted_sd.get("time_guidance_embed.timestep_embedder.linear_2.weight") + if timestep_linear2 is not None: + in_features2 = timestep_linear2.shape[1] + out_features2 = timestep_linear2.shape[0] + converted_sd["time_guidance_embed.guidance_embedder.linear_2.weight"] = torch.zeros( + out_features2, in_features2, dtype=torch.bfloat16 + ) + + # Convert to bfloat16 and load + for k in converted_sd.keys(): + converted_sd[k] = converted_sd[k].to(torch.bfloat16) + + # Load the state dict - guidance weights were already initialized above if missing + model.load_state_dict(converted_sd, assign=True) + + return model + + def _convert_flux2_bfl_to_diffusers(self, sd: dict) -> dict: + """Convert FLUX.2 BFL format state dict to diffusers format. + + Based on diffusers convert_flux2_to_diffusers.py key mappings. + """ + converted = {} + + # Basic key renames + key_renames = { + "img_in.weight": "x_embedder.weight", + "txt_in.weight": "context_embedder.weight", + "time_in.in_layer.weight": "time_guidance_embed.timestep_embedder.linear_1.weight", + "time_in.out_layer.weight": "time_guidance_embed.timestep_embedder.linear_2.weight", + "guidance_in.in_layer.weight": "time_guidance_embed.guidance_embedder.linear_1.weight", + "guidance_in.out_layer.weight": "time_guidance_embed.guidance_embedder.linear_2.weight", + "double_stream_modulation_img.lin.weight": "double_stream_modulation_img.linear.weight", + "double_stream_modulation_txt.lin.weight": "double_stream_modulation_txt.linear.weight", + "single_stream_modulation.lin.weight": "single_stream_modulation.linear.weight", + "final_layer.linear.weight": "proj_out.weight", + "final_layer.adaLN_modulation.1.weight": "norm_out.linear.weight", + } + + for old_key, tensor in sd.items(): + new_key = old_key + + # Apply basic renames + if old_key in key_renames: + new_key = key_renames[old_key] + # Apply scale-shift swap for adaLN modulation weights + # BFL and diffusers use different parameter ordering for AdaLayerNorm + if old_key == "final_layer.adaLN_modulation.1.weight": + tensor = self._swap_scale_shift(tensor) + converted[new_key] = tensor + continue + + # Convert double_blocks.X.* to transformer_blocks.X.* + if old_key.startswith("double_blocks."): + new_key = self._convert_double_block_key(old_key, tensor, converted) + if new_key is None: + continue # Key was handled specially + # Convert single_blocks.X.* to single_transformer_blocks.X.* + elif old_key.startswith("single_blocks."): + new_key = self._convert_single_block_key(old_key, tensor, converted) + if new_key is None: + continue # Key was handled specially + + if new_key != old_key or new_key not in converted: + converted[new_key] = tensor + + return converted + + def _convert_double_block_key(self, key: str, tensor: torch.Tensor, converted: dict) -> str | None: + """Convert double_blocks key to transformer_blocks format.""" + parts = key.split(".") + block_idx = parts[1] + rest = ".".join(parts[2:]) + + prefix = f"transformer_blocks.{block_idx}" + + # Attention QKV conversion - BFL uses fused qkv, diffusers uses separate + if "img_attn.qkv.weight" in rest: + # Split fused QKV into separate Q, K, V + # Defensive check: ensure tensor has at least 1 dimension and can be split into 3 + if tensor.dim() < 1 or tensor.shape[0] % 3 != 0: + # Skip malformed tensors (might be metadata or corrupted) + return key + q, k, v = tensor.chunk(3, dim=0) + converted[f"{prefix}.attn.to_q.weight"] = q + converted[f"{prefix}.attn.to_k.weight"] = k + converted[f"{prefix}.attn.to_v.weight"] = v + return None + elif "txt_attn.qkv.weight" in rest: + # Defensive check + if tensor.dim() < 1 or tensor.shape[0] % 3 != 0: + return key + q, k, v = tensor.chunk(3, dim=0) + converted[f"{prefix}.attn.add_q_proj.weight"] = q + converted[f"{prefix}.attn.add_k_proj.weight"] = k + converted[f"{prefix}.attn.add_v_proj.weight"] = v + return None + + # Attention output projection + if "img_attn.proj.weight" in rest: + return f"{prefix}.attn.to_out.0.weight" + elif "txt_attn.proj.weight" in rest: + return f"{prefix}.attn.to_add_out.weight" + + # Attention norms + if "img_attn.norm.query_norm.scale" in rest or "img_attn.norm.query_norm.weight" in rest: + return f"{prefix}.attn.norm_q.weight" + elif "img_attn.norm.key_norm.scale" in rest or "img_attn.norm.key_norm.weight" in rest: + return f"{prefix}.attn.norm_k.weight" + elif "txt_attn.norm.query_norm.scale" in rest or "txt_attn.norm.query_norm.weight" in rest: + return f"{prefix}.attn.norm_added_q.weight" + elif "txt_attn.norm.key_norm.scale" in rest or "txt_attn.norm.key_norm.weight" in rest: + return f"{prefix}.attn.norm_added_k.weight" + + # MLP layers + if "img_mlp.0.weight" in rest: + return f"{prefix}.ff.linear_in.weight" + elif "img_mlp.2.weight" in rest: + return f"{prefix}.ff.linear_out.weight" + elif "txt_mlp.0.weight" in rest: + return f"{prefix}.ff_context.linear_in.weight" + elif "txt_mlp.2.weight" in rest: + return f"{prefix}.ff_context.linear_out.weight" + + return key + + def _convert_single_block_key(self, key: str, tensor: torch.Tensor, converted: dict) -> str | None: + """Convert single_blocks key to single_transformer_blocks format.""" + parts = key.split(".") + block_idx = parts[1] + rest = ".".join(parts[2:]) + + prefix = f"single_transformer_blocks.{block_idx}" + + # linear1 is the fused QKV+MLP projection + if "linear1.weight" in rest: + return f"{prefix}.attn.to_qkv_mlp_proj.weight" + elif "linear2.weight" in rest: + return f"{prefix}.attn.to_out.weight" + + # Norms + if "norm.query_norm.scale" in rest or "norm.query_norm.weight" in rest: + return f"{prefix}.attn.norm_q.weight" + elif "norm.key_norm.scale" in rest or "norm.key_norm.weight" in rest: + return f"{prefix}.attn.norm_k.weight" + + return key + + def _swap_scale_shift(self, weight: torch.Tensor) -> torch.Tensor: + """Swap scale and shift in AdaLayerNorm weights. + + BFL and diffusers use different parameter ordering for AdaLayerNorm. + This function swaps the two halves of the weight tensor. + + Args: + weight: Weight tensor of shape (out_features,) or (out_features, in_features) + + Returns: + Weight tensor with scale and shift swapped. + """ + # Defensive check: ensure tensor can be split + if weight.dim() < 1 or weight.shape[0] % 2 != 0: + return weight + # Split in half along the first dimension and swap + shift, scale = weight.chunk(2, dim=0) + return torch.cat([scale, shift], dim=0) + + def _dequantize_fp8_weights(self, sd: dict) -> dict: + """Dequantize FP8 quantized weights in the state dict. + + ComfyUI and some FLUX.2 models store quantized weights as: + - layer.weight: quantized FP8 data + - layer.weight_scale: scale factor (FP32 scalar or per-channel) + + Dequantization formula: dequantized = weight.to(float) * weight_scale + + Also handles FP8 tensors stored with float8_e4m3fn dtype by converting to float. + """ + # Check for ComfyUI-style scale factors + weight_scale_keys = [k for k in sd.keys() if isinstance(k, str) and k.endswith(".weight_scale")] + + for scale_key in weight_scale_keys: + # Get the corresponding weight key + weight_key = scale_key.replace(".weight_scale", ".weight") + if weight_key in sd: + weight = sd[weight_key] + scale = sd[scale_key] + + # Dequantize: convert FP8 to float and multiply by scale + # Note: Float8 types require .float() instead of .to(torch.float32) + weight_float = weight.float() + scale = scale.float() + + # Handle block-wise quantization where scale may have different shape + if scale.dim() > 0 and scale.shape != weight_float.shape and scale.numel() > 1: + for dim in range(len(weight_float.shape)): + if dim < len(scale.shape) and scale.shape[dim] != weight_float.shape[dim]: + block_size = weight_float.shape[dim] // scale.shape[dim] + if block_size > 1: + scale = scale.repeat_interleave(block_size, dim=dim) + + sd[weight_key] = weight_float * scale + + # Filter out scale metadata keys and other FP8 metadata + keys_to_remove = [ + k + for k in sd.keys() + if isinstance(k, str) + and (k.endswith(".weight_scale") or k.endswith(".scale_weight") or "comfy_quant" in k or k == "scaled_fp8") + ] + for k in keys_to_remove: + del sd[k] + + # Handle native FP8 tensors (float8_e4m3fn dtype) that aren't already dequantized + # Also filter out 0-dimensional tensors (scalars) which are typically metadata + keys_to_convert = [] + keys_to_remove_scalars = [] + for key in list(sd.keys()): + tensor = sd[key] + if hasattr(tensor, "dim"): + if tensor.dim() == 0: + # 0-dimensional tensor (scalar) - likely metadata, remove it + keys_to_remove_scalars.append(key) + elif hasattr(tensor, "dtype") and "float8" in str(tensor.dtype): + # Native FP8 tensor - mark for conversion + keys_to_convert.append(key) + + for k in keys_to_remove_scalars: + del sd[k] + + for key in keys_to_convert: + # Convert FP8 tensor to float32 + sd[key] = sd[key].float() + + return sd + + +@ModelLoaderRegistry.register(base=BaseModelType.Flux2, type=ModelType.Main, format=ModelFormat.GGUFQuantized) +class Flux2GGUFCheckpointModel(ModelLoader): + """Class to load GGUF-quantized FLUX.2 transformer models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, Main_GGUF_Flux2_Config): + raise ValueError("Only Main_GGUF_Flux2_Config models are currently supported here.") + + match submodel_type: + case SubModelType.Transformer: + return self._load_from_singlefile(config) + + raise ValueError( + f"Only Transformer submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}" + ) + + def _load_from_singlefile( + self, + config: Main_GGUF_Flux2_Config, + ) -> AnyModel: + from diffusers import Flux2Transformer2DModel + + model_path = Path(config.path) + + # Load GGUF state dict + sd = gguf_sd_loader(model_path, compute_dtype=torch.bfloat16) + + # Check if keys have ComfyUI-style prefix and strip if needed + prefix_to_strip = None + for prefix in ["model.diffusion_model.", "diffusion_model."]: + if any(k.startswith(prefix) for k in sd.keys() if isinstance(k, str)): + prefix_to_strip = prefix + break + + if prefix_to_strip: + sd = { + (k[len(prefix_to_strip) :] if isinstance(k, str) and k.startswith(prefix_to_strip) else k): v + for k, v in sd.items() + } + + # Convert BFL format state dict to diffusers format + converted_sd = self._convert_flux2_bfl_to_diffusers(sd) + + # Detect architecture from checkpoint keys + double_block_indices = [ + int(k.split(".")[1]) + for k in converted_sd.keys() + if isinstance(k, str) and k.startswith("transformer_blocks.") + ] + single_block_indices = [ + int(k.split(".")[1]) + for k in converted_sd.keys() + if isinstance(k, str) and k.startswith("single_transformer_blocks.") + ] + + num_layers = max(double_block_indices) + 1 if double_block_indices else 5 + num_single_layers = max(single_block_indices) + 1 if single_block_indices else 20 + + # Get dimensions from weights + # context_embedder.weight shape: [hidden_size, joint_attention_dim] + context_embedder_weight = converted_sd.get("context_embedder.weight") + if context_embedder_weight is not None: + if hasattr(context_embedder_weight, "tensor_shape"): + hidden_size = context_embedder_weight.tensor_shape[0] + joint_attention_dim = context_embedder_weight.tensor_shape[1] + else: + hidden_size = context_embedder_weight.shape[0] + joint_attention_dim = context_embedder_weight.shape[1] + else: + # Default to Klein 4B dimensions + hidden_size = 3072 + joint_attention_dim = 7680 + + x_embedder_weight = converted_sd.get("x_embedder.weight") + if x_embedder_weight is not None: + in_channels = ( + x_embedder_weight.tensor_shape[1] + if hasattr(x_embedder_weight, "tensor_shape") + else x_embedder_weight.shape[1] + ) + else: + in_channels = 128 + + # Calculate num_attention_heads from hidden_size + # Klein 4B: hidden_size=3072, num_attention_heads=24 (3072/128=24) + # Klein 9B: hidden_size=4096, num_attention_heads=32 (4096/128=32) + attention_head_dim = 128 + num_attention_heads = hidden_size // attention_head_dim + + # Klein models don't have guidance embeddings - check if they're in the checkpoint + has_guidance = "time_guidance_embed.guidance_embedder.linear_1.weight" in converted_sd + + # Create model with detected configuration + with SilenceWarnings(): + with accelerate.init_empty_weights(): + model = Flux2Transformer2DModel( + in_channels=in_channels, + out_channels=in_channels, + num_layers=num_layers, + num_single_layers=num_single_layers, + attention_head_dim=attention_head_dim, + num_attention_heads=num_attention_heads, + joint_attention_dim=joint_attention_dim, + patch_size=1, + ) + + # If Klein model without guidance, initialize guidance embedder with zeros + if not has_guidance: + timestep_linear1 = converted_sd.get("time_guidance_embed.timestep_embedder.linear_1.weight") + if timestep_linear1 is not None: + in_features = ( + timestep_linear1.tensor_shape[1] + if hasattr(timestep_linear1, "tensor_shape") + else timestep_linear1.shape[1] + ) + out_features = ( + timestep_linear1.tensor_shape[0] + if hasattr(timestep_linear1, "tensor_shape") + else timestep_linear1.shape[0] + ) + converted_sd["time_guidance_embed.guidance_embedder.linear_1.weight"] = torch.zeros( + out_features, in_features, dtype=torch.bfloat16 + ) + timestep_linear2 = converted_sd.get("time_guidance_embed.timestep_embedder.linear_2.weight") + if timestep_linear2 is not None: + in_features2 = ( + timestep_linear2.tensor_shape[1] + if hasattr(timestep_linear2, "tensor_shape") + else timestep_linear2.shape[1] + ) + out_features2 = ( + timestep_linear2.tensor_shape[0] + if hasattr(timestep_linear2, "tensor_shape") + else timestep_linear2.shape[0] + ) + converted_sd["time_guidance_embed.guidance_embedder.linear_2.weight"] = torch.zeros( + out_features2, in_features2, dtype=torch.bfloat16 + ) + + model.load_state_dict(converted_sd, assign=True) + return model + + def _convert_flux2_bfl_to_diffusers(self, sd: dict) -> dict: + """Convert FLUX.2 BFL format state dict to diffusers format.""" + converted = {} + + key_renames = { + "img_in.weight": "x_embedder.weight", + "txt_in.weight": "context_embedder.weight", + "time_in.in_layer.weight": "time_guidance_embed.timestep_embedder.linear_1.weight", + "time_in.out_layer.weight": "time_guidance_embed.timestep_embedder.linear_2.weight", + "guidance_in.in_layer.weight": "time_guidance_embed.guidance_embedder.linear_1.weight", + "guidance_in.out_layer.weight": "time_guidance_embed.guidance_embedder.linear_2.weight", + "double_stream_modulation_img.lin.weight": "double_stream_modulation_img.linear.weight", + "double_stream_modulation_txt.lin.weight": "double_stream_modulation_txt.linear.weight", + "single_stream_modulation.lin.weight": "single_stream_modulation.linear.weight", + "final_layer.linear.weight": "proj_out.weight", + "final_layer.adaLN_modulation.1.weight": "norm_out.linear.weight", + } + + for old_key, tensor in sd.items(): + new_key = old_key + + if old_key in key_renames: + new_key = key_renames[old_key] + if old_key == "final_layer.adaLN_modulation.1.weight": + tensor = self._swap_scale_shift(tensor) + converted[new_key] = tensor + continue + + if old_key.startswith("double_blocks."): + new_key = self._convert_double_block_key(old_key, tensor, converted) + if new_key is None: + continue + elif old_key.startswith("single_blocks."): + new_key = self._convert_single_block_key(old_key, tensor, converted) + if new_key is None: + continue + + if new_key != old_key or new_key not in converted: + converted[new_key] = tensor + + return converted + + def _convert_double_block_key(self, key: str, tensor, converted: dict) -> str | None: + parts = key.split(".") + block_idx = parts[1] + rest = ".".join(parts[2:]) + prefix = f"transformer_blocks.{block_idx}" + + if "img_attn.qkv.weight" in rest: + q, k, v = self._chunk_tensor(tensor, 3) + converted[f"{prefix}.attn.to_q.weight"] = q + converted[f"{prefix}.attn.to_k.weight"] = k + converted[f"{prefix}.attn.to_v.weight"] = v + return None + elif "txt_attn.qkv.weight" in rest: + q, k, v = self._chunk_tensor(tensor, 3) + converted[f"{prefix}.attn.add_q_proj.weight"] = q + converted[f"{prefix}.attn.add_k_proj.weight"] = k + converted[f"{prefix}.attn.add_v_proj.weight"] = v + return None + + if "img_attn.proj.weight" in rest: + return f"{prefix}.attn.to_out.0.weight" + elif "txt_attn.proj.weight" in rest: + return f"{prefix}.attn.to_add_out.weight" + + if "img_attn.norm.query_norm.scale" in rest or "img_attn.norm.query_norm.weight" in rest: + return f"{prefix}.attn.norm_q.weight" + elif "img_attn.norm.key_norm.scale" in rest or "img_attn.norm.key_norm.weight" in rest: + return f"{prefix}.attn.norm_k.weight" + elif "txt_attn.norm.query_norm.scale" in rest or "txt_attn.norm.query_norm.weight" in rest: + return f"{prefix}.attn.norm_added_q.weight" + elif "txt_attn.norm.key_norm.scale" in rest or "txt_attn.norm.key_norm.weight" in rest: + return f"{prefix}.attn.norm_added_k.weight" + + if "img_mlp.0.weight" in rest: + return f"{prefix}.ff.linear_in.weight" + elif "img_mlp.2.weight" in rest: + return f"{prefix}.ff.linear_out.weight" + elif "txt_mlp.0.weight" in rest: + return f"{prefix}.ff_context.linear_in.weight" + elif "txt_mlp.2.weight" in rest: + return f"{prefix}.ff_context.linear_out.weight" + + return key + + def _convert_single_block_key(self, key: str, tensor, converted: dict) -> str | None: + parts = key.split(".") + block_idx = parts[1] + rest = ".".join(parts[2:]) + prefix = f"single_transformer_blocks.{block_idx}" + + if "linear1.weight" in rest: + return f"{prefix}.attn.to_qkv_mlp_proj.weight" + elif "linear2.weight" in rest: + return f"{prefix}.attn.to_out.weight" + + if "norm.query_norm.scale" in rest or "norm.query_norm.weight" in rest: + return f"{prefix}.attn.norm_q.weight" + elif "norm.key_norm.scale" in rest or "norm.key_norm.weight" in rest: + return f"{prefix}.attn.norm_k.weight" + + return key + + def _chunk_tensor(self, tensor, chunks: int): + """Chunk a tensor, handling both regular tensors and GGUF quantized tensors.""" + if hasattr(tensor, "get_dequantized_tensor"): + # GGUF quantized tensor - dequantize first, then chunk + # This loses quantization for the split weights, but is necessary + # because diffusers uses separate Q/K/V projections + tensor = tensor.get_dequantized_tensor() + return tensor.chunk(chunks, dim=0) + + def _swap_scale_shift(self, weight) -> torch.Tensor: + """Swap scale and shift in AdaLayerNorm weights.""" + if hasattr(weight, "get_dequantized_tensor"): + # For GGUF, dequantize first + weight = weight.get_dequantized_tensor() + shift, scale = weight.chunk(2, dim=0) + return torch.cat([scale, shift], dim=0) + + +@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.ControlNet, format=ModelFormat.Checkpoint) +@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.ControlNet, format=ModelFormat.Diffusers) +class FluxControlnetModel(ModelLoader): + """Class to load FLUX ControlNet models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if isinstance(config, ControlNet_Checkpoint_Config_Base): + model_path = Path(config.path) + elif isinstance(config, ControlNet_Diffusers_Config_Base): + # If this is a diffusers directory, we simply ignore the config file and load from the weight file. + model_path = Path(config.path) / "diffusion_pytorch_model.safetensors" + else: + raise ValueError(f"Unexpected ControlNet model config type: {type(config)}") + + sd = load_file(model_path) + + # Detect the FLUX ControlNet model type from the state dict. + if is_state_dict_xlabs_controlnet(sd): + return self._load_xlabs_controlnet(sd) + elif is_state_dict_instantx_controlnet(sd): + return self._load_instantx_controlnet(sd) + else: + raise ValueError("Do not recognize the state dict as an XLabs or InstantX ControlNet model.") + + def _load_xlabs_controlnet(self, sd: dict[str, torch.Tensor]) -> AnyModel: + with accelerate.init_empty_weights(): + # HACK(ryand): Is it safe to assume dev here? + model = XLabsControlNetFlux(get_flux_transformers_params(FluxVariantType.Dev)) + + model.load_state_dict(sd, assign=True) + return model + + def _load_instantx_controlnet(self, sd: dict[str, torch.Tensor]) -> AnyModel: + sd = convert_diffusers_instantx_state_dict_to_bfl_format(sd) + flux_params = infer_flux_params_from_state_dict(sd) + num_control_modes = infer_instantx_num_control_modes_from_state_dict(sd) + + with accelerate.init_empty_weights(): + model = InstantXControlNetFlux(flux_params, num_control_modes) + + model.load_state_dict(sd, assign=True) + return model + + +@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.IPAdapter, format=ModelFormat.Checkpoint) +class FluxIpAdapterModel(ModelLoader): + """Class to load FLUX IP-Adapter models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, IPAdapter_Checkpoint_Config_Base): + raise ValueError(f"Unexpected model config type: {type(config)}.") + + sd = load_file(Path(config.path)) + + params = infer_xlabs_ip_adapter_params_from_state_dict(sd) + + with accelerate.init_empty_weights(): + model = XlabsIpAdapterFlux(params=params) + + model.load_xlabs_state_dict(sd, assign=True) + return model + + +@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.FluxRedux, format=ModelFormat.Checkpoint) +class FluxReduxModelLoader(ModelLoader): + """Class to load FLUX Redux models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, FLUXRedux_Checkpoint_Config): + raise ValueError(f"Unexpected model config type: {type(config)}.") + + sd = load_file(Path(config.path)) + + with accelerate.init_empty_weights(): + model = FluxReduxModel() + + model.load_state_dict(sd, assign=True) + model.to(dtype=torch.bfloat16) + return model diff --git a/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py b/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py new file mode 100644 index 00000000000..7e87869c9e3 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py @@ -0,0 +1,104 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for simple diffusers model loading in InvokeAI.""" + +import sys +from pathlib import Path +from typing import Any, Optional + +from diffusers.configuration_utils import ConfigMixin +from diffusers.models.modeling_utils import ModelMixin + +from invokeai.backend.model_manager.configs.base import Diffusers_Config_Base +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.load.load_default import ModelLoader +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.taxonomy import ( + AnyModel, + BaseModelType, + ModelFormat, + ModelType, + SubModelType, +) + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.T2IAdapter, format=ModelFormat.Diffusers) +class GenericDiffusersLoader(ModelLoader): + """Class to load simple diffusers models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + model_path = Path(config.path) + 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}") + repo_variant = config.repo_variant if isinstance(config, Diffusers_Config_Base) else None + variant = repo_variant.value if repo_variant else None + try: + result: AnyModel = model_class.from_pretrained( + model_path, torch_dtype=self._torch_dtype, variant=variant, local_files_only=True + ) + except OSError as e: + if variant and "no file named" in str( + e + ): # try without the variant, just in case user's preferences changed + result = model_class.from_pretrained(model_path, torch_dtype=self._torch_dtype, local_files_only=True) + else: + raise e + result = self._apply_fp8_layerwise_casting(result, config, submodel_type) + return result + + # TO DO: Add exception handling + def get_hf_load_class(self, model_path: Path, submodel_type: Optional[SubModelType] = None) -> ModelMixin: + """Given the model path and submodel, returns the diffusers ModelMixin subclass needed to load.""" + result = None + if submodel_type: + try: + config = self._load_diffusers_config(model_path, config_name="model_index.json") + module, class_name = config[submodel_type.value] + result = self._hf_definition_to_type(module=module, class_name=class_name) + except KeyError as e: + raise ValueError(f'The "{submodel_type}" submodel is not available for this model.') from e + else: + try: + config = self._load_diffusers_config(model_path, config_name="config.json") + if class_name := config.get("_class_name"): + result = self._hf_definition_to_type(module="diffusers", class_name=class_name) + elif class_name := config.get("architectures"): + result = self._hf_definition_to_type(module="transformers", class_name=class_name[0]) + else: + raise RuntimeError("Unable to decipher Load Class based on given config.json") + except KeyError as e: + raise ValueError("An expected config.json file is missing from this model.") from e + assert result is not None + return result + + # TO DO: Add exception handling + def _hf_definition_to_type(self, module: str, class_name: str) -> ModelMixin: # fix with correct type + if module in [ + "diffusers", + "transformers", + "invokeai.backend.quantization.fast_quantized_transformers_model", + "invokeai.backend.quantization.fast_quantized_diffusion_model", + ]: + res_type = sys.modules[module] + else: + res_type = sys.modules["diffusers"].pipelines + result: ModelMixin = getattr(res_type, class_name) + return result + + def _load_diffusers_config(self, model_path: Path, config_name: str = "config.json") -> dict[str, Any]: + return ConfigLoader.load_config(model_path, config_name=config_name) + + +class ConfigLoader(ConfigMixin): + """Subclass of ConfigMixin for loading diffusers configuration files.""" + + @classmethod + def load_config(cls, *args: Any, **kwargs: Any) -> dict[str, Any]: # pyright: ignore [reportIncompatibleMethodOverride] + """Load a diffusrs ConfigMixin configuration.""" + cls.config_name = kwargs.pop("config_name") + # TODO(psyche): the types on this diffusers method are not correct + return super().load_config(*args, **kwargs) # type: ignore diff --git a/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py b/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py new file mode 100644 index 00000000000..d133a36498c --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py @@ -0,0 +1,34 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for IP Adapter model loading in InvokeAI.""" + +from pathlib import Path +from typing import Optional + +import torch + +from invokeai.backend.ip_adapter.ip_adapter import build_ip_adapter +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.load import ModelLoader, ModelLoaderRegistry +from invokeai.backend.model_manager.taxonomy import AnyModel, BaseModelType, ModelFormat, ModelType, SubModelType +from invokeai.backend.raw_model import RawModel + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.IPAdapter, format=ModelFormat.InvokeAI) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.IPAdapter, format=ModelFormat.Checkpoint) +class IPAdapterInvokeAILoader(ModelLoader): + """Class to load IP Adapter diffusers models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if submodel_type is not None: + raise ValueError("There are no submodels in an IP-Adapter model.") + model_path = Path(config.path) + model: RawModel = build_ip_adapter( + ip_adapter_ckpt_path=model_path, + device=torch.device("cpu"), + dtype=self._torch_dtype, + ) + return model diff --git a/invokeai/backend/model_manager/load/model_loaders/llava_onevision.py b/invokeai/backend/model_manager/load/model_loaders/llava_onevision.py new file mode 100644 index 00000000000..e459bbf2bb1 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/llava_onevision.py @@ -0,0 +1,29 @@ +from pathlib import Path +from typing import Optional + +from transformers import LlavaOnevisionForConditionalGeneration + +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.load.load_default import ModelLoader +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.taxonomy import AnyModel, BaseModelType, ModelFormat, ModelType, SubModelType + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.LlavaOnevision, format=ModelFormat.Diffusers) +class LlavaOnevisionModelLoader(ModelLoader): + """Class for loading LLaVA Onevision VLLM models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if submodel_type is not None: + raise ValueError("Unexpected submodel requested for LLaVA OneVision model.") + + model_path = Path(config.path) + model = LlavaOnevisionForConditionalGeneration.from_pretrained( + model_path, local_files_only=True, torch_dtype=self._torch_dtype + ) + assert isinstance(model, LlavaOnevisionForConditionalGeneration) + return model diff --git a/invokeai/backend/model_manager/load/model_loaders/lora.py b/invokeai/backend/model_manager/load/model_loaders/lora.py new file mode 100644 index 00000000000..15dfa376179 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/lora.py @@ -0,0 +1,198 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for LoRA model loading in InvokeAI.""" + +from logging import Logger +from pathlib import Path +from typing import Optional + +import torch +from safetensors.torch import load_file + +from invokeai.app.services.config import InvokeAIAppConfig +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.load.load_default import ModelLoader +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.model_manager.omi.omi import convert_from_omi +from invokeai.backend.model_manager.taxonomy import ( + AnyModel, + BaseModelType, + ModelFormat, + ModelType, + SubModelType, +) +from invokeai.backend.patches.lora_conversions.anima_lora_conversion_utils import lora_model_from_anima_state_dict +from invokeai.backend.patches.lora_conversions.flux_aitoolkit_lora_conversion_utils import ( + is_state_dict_likely_in_flux_aitoolkit_format, + lora_model_from_flux_aitoolkit_state_dict, +) +from invokeai.backend.patches.lora_conversions.flux_bfl_peft_lora_conversion_utils import ( + is_state_dict_likely_in_flux_bfl_peft_format, + lora_model_from_flux2_bfl_peft_state_dict, + lora_model_from_flux_bfl_peft_state_dict, +) +from invokeai.backend.patches.lora_conversions.flux_control_lora_utils import ( + is_state_dict_likely_flux_control, + lora_model_from_flux_control_state_dict, +) +from invokeai.backend.patches.lora_conversions.flux_diffusers_lora_conversion_utils import ( + is_state_dict_flux2_diffusers_format, + is_state_dict_likely_in_flux_diffusers_format, + lora_model_from_flux2_diffusers_state_dict, + lora_model_from_flux_diffusers_state_dict, +) +from invokeai.backend.patches.lora_conversions.flux_kohya_lora_conversion_utils import ( + is_state_dict_likely_in_flux_kohya_format, + lora_model_from_flux_kohya_state_dict, +) +from invokeai.backend.patches.lora_conversions.flux_onetrainer_bfl_lora_conversion_utils import ( + is_state_dict_likely_in_flux_onetrainer_bfl_format, + lora_model_from_flux_onetrainer_bfl_state_dict, +) +from invokeai.backend.patches.lora_conversions.flux_onetrainer_lora_conversion_utils import ( + is_state_dict_likely_in_flux_onetrainer_format, + lora_model_from_flux_onetrainer_state_dict, +) +from invokeai.backend.patches.lora_conversions.flux_xlabs_lora_conversion_utils import ( + is_state_dict_likely_in_flux_xlabs_format, + lora_model_from_flux_xlabs_state_dict, +) +from invokeai.backend.patches.lora_conversions.peft_adapter_utils import normalize_peft_adapter_names +from invokeai.backend.patches.lora_conversions.qwen_image_lora_conversion_utils import ( + lora_model_from_qwen_image_state_dict, +) +from invokeai.backend.patches.lora_conversions.sd_lora_conversion_utils import lora_model_from_sd_state_dict +from invokeai.backend.patches.lora_conversions.sdxl_lora_conversion_utils import convert_sdxl_keys_to_diffusers_format +from invokeai.backend.patches.lora_conversions.z_image_lora_conversion_utils import lora_model_from_z_image_state_dict + + +@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.LoRA, format=ModelFormat.OMI) +@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusionXL, type=ModelType.LoRA, format=ModelFormat.OMI) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.LoRA, format=ModelFormat.Diffusers) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.LoRA, format=ModelFormat.LyCORIS) +@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.ControlLoRa, format=ModelFormat.LyCORIS) +@ModelLoaderRegistry.register(base=BaseModelType.Flux, type=ModelType.ControlLoRa, format=ModelFormat.Diffusers) +class LoRALoader(ModelLoader): + """Class to load LoRA models.""" + + # We cheat a little bit to get access to the model base + def __init__( + self, + app_config: InvokeAIAppConfig, + logger: Logger, + ram_cache: ModelCache, + ): + """Initialize the loader.""" + super().__init__(app_config, logger, ram_cache) + self._model_base: Optional[BaseModelType] = None + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if submodel_type is not None: + raise ValueError("There are no submodels in a LoRA model.") + model_path = Path(config.path) + assert self._model_base is not None + + # Load the state dict from the model file. + if model_path.suffix == ".safetensors": + state_dict = load_file(model_path.absolute().as_posix(), device="cpu") + else: + state_dict = torch.load(model_path, map_location="cpu") + + # Strip 'bundle_emb' keys - these are unused and currently cause downstream errors. + # To revisit later to determine if they're needed/useful. + state_dict = {k: v for k, v in state_dict.items() if not k.startswith("bundle_emb")} + + # Normalize PEFT named-adapter keys (e.g. `lora_A.default.weight` → `lora_A.weight`) + # so the downstream format detectors and converters see canonical PEFT keys. + state_dict = normalize_peft_adapter_names(state_dict) + + # At the time of writing, we support the OMI standard for base models Flux and SDXL + if config.format == ModelFormat.OMI and self._model_base in [ + BaseModelType.StableDiffusionXL, + BaseModelType.Flux, + ]: + state_dict = convert_from_omi(state_dict, config.base) # type: ignore + + # Apply state_dict key conversions, if necessary. + if self._model_base == BaseModelType.StableDiffusionXL: + state_dict = convert_sdxl_keys_to_diffusers_format(state_dict) + model = lora_model_from_sd_state_dict(state_dict=state_dict) + elif self._model_base in (BaseModelType.Flux, BaseModelType.Flux2): + if config.format is ModelFormat.OMI: + # HACK(ryand): We set alpha=None for diffusers PEFT format models. These models are typically + # distributed as a single file without the associated metadata containing the alpha value. We chose + # alpha=None, because this is treated as alpha=rank internally in `LoRALayerBase.scale()`. alpha=rank + # is a popular choice. For example, in the diffusers training scripts: + # https://github.com/huggingface/diffusers/blob/main/examples/dreambooth/train_dreambooth_lora_flux.py#L1194 + # + # We assume the same for LyCORIS models in diffusers key format. + model = lora_model_from_flux_diffusers_state_dict(state_dict=state_dict, alpha=None) + elif config.format is ModelFormat.LyCORIS: + if is_state_dict_likely_in_flux_diffusers_format(state_dict=state_dict): + if is_state_dict_flux2_diffusers_format(state_dict=state_dict): + # Flux2 Klein native diffusers naming (to_qkv_mlp_proj, ff.linear_in, etc.) + model = lora_model_from_flux2_diffusers_state_dict(state_dict=state_dict, alpha=None) + else: + # Flux.1 diffusers naming (to_q/to_k/to_v, ff.net.0.proj, etc.) + model = lora_model_from_flux_diffusers_state_dict(state_dict=state_dict, alpha=None) + elif is_state_dict_likely_in_flux_kohya_format(state_dict=state_dict): + model = lora_model_from_flux_kohya_state_dict(state_dict=state_dict) + elif is_state_dict_likely_in_flux_onetrainer_bfl_format(state_dict=state_dict): + model = lora_model_from_flux_onetrainer_bfl_state_dict(state_dict=state_dict) + elif is_state_dict_likely_in_flux_onetrainer_format(state_dict=state_dict): + model = lora_model_from_flux_onetrainer_state_dict(state_dict=state_dict) + elif is_state_dict_likely_flux_control(state_dict=state_dict): + model = lora_model_from_flux_control_state_dict(state_dict=state_dict) + elif is_state_dict_likely_in_flux_aitoolkit_format(state_dict=state_dict): + model = lora_model_from_flux_aitoolkit_state_dict(state_dict=state_dict) + elif is_state_dict_likely_in_flux_xlabs_format(state_dict=state_dict): + model = lora_model_from_flux_xlabs_state_dict(state_dict=state_dict) + elif is_state_dict_likely_in_flux_bfl_peft_format(state_dict=state_dict): + if self._model_base == BaseModelType.Flux2: + # FLUX.2 Klein uses Flux2Transformer2DModel (diffusers naming), + # so we need to convert BFL keys to diffusers naming. + model = lora_model_from_flux2_bfl_peft_state_dict(state_dict=state_dict, alpha=None) + else: + # FLUX.1 uses BFL Flux class, so BFL keys work directly. + model = lora_model_from_flux_bfl_peft_state_dict(state_dict=state_dict, alpha=None) + else: + raise ValueError("LoRA model is in unsupported FLUX format") + else: + raise ValueError(f"LoRA model is in unsupported FLUX format: {config.format}") + elif self._model_base in [BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2]: + # Currently, we don't apply any conversions for SD1 and SD2 LoRA models. + model = lora_model_from_sd_state_dict(state_dict=state_dict) + elif self._model_base == BaseModelType.ZImage: + # Z-Image LoRAs use diffusers PEFT format with transformer and/or Qwen3 encoder layers. + # We set alpha=None to use rank as alpha (common default). + model = lora_model_from_z_image_state_dict(state_dict=state_dict, alpha=None) + elif self._model_base == BaseModelType.QwenImage: + model = lora_model_from_qwen_image_state_dict(state_dict=state_dict, alpha=None) + elif self._model_base == BaseModelType.Anima: + # Anima LoRAs use Kohya-style or diffusers PEFT format targeting Cosmos DiT blocks. + model = lora_model_from_anima_state_dict(state_dict=state_dict, alpha=None) + else: + raise ValueError(f"Unsupported LoRA base model: {self._model_base}") + + model.to(dtype=self._torch_dtype) + return model + + def _get_model_path(self, config: AnyModelConfig) -> Path: + # cheating a little - we remember this variable for using in the subsequent call to _load_model() + self._model_base = config.base + + model_base_path = self._app_config.models_path + model_path = model_base_path / config.path + + if config.format == ModelFormat.Diffusers: + for ext in ["safetensors", "bin"]: # return path to the safetensors file inside the folder + path = model_base_path / config.path / f"pytorch_lora_weights.{ext}" + if path.exists(): + model_path = path + break + + return model_path.resolve() diff --git a/invokeai/backend/model_manager/load/model_loaders/onnx.py b/invokeai/backend/model_manager/load/model_loaders/onnx.py new file mode 100644 index 00000000000..6ffab997cf3 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/onnx.py @@ -0,0 +1,43 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for Onnx model loading in InvokeAI.""" + +# This should work the same as Stable Diffusion pipelines +from pathlib import Path +from typing import Optional + +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader +from invokeai.backend.model_manager.taxonomy import ( + AnyModel, + BaseModelType, + ModelFormat, + ModelType, + SubModelType, +) + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.ONNX, format=ModelFormat.ONNX) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.ONNX, format=ModelFormat.Olive) +class OnnyxDiffusersModel(GenericDiffusersLoader): + """Class to load onnx models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not submodel_type is not None: + raise Exception("A submodel type must be provided when loading onnx pipelines.") + model_path = Path(config.path) + load_class = self.get_hf_load_class(model_path, submodel_type) + repo_variant = getattr(config, "repo_variant", None) + variant = repo_variant.value if repo_variant else None + model_path = model_path / submodel_type.value + result: AnyModel = load_class.from_pretrained( + model_path, + torch_dtype=self._torch_dtype, + variant=variant, + local_files_only=True, + ) + return result diff --git a/invokeai/backend/model_manager/load/model_loaders/qwen_image.py b/invokeai/backend/model_manager/load/model_loaders/qwen_image.py new file mode 100644 index 00000000000..0e86afa4e53 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/qwen_image.py @@ -0,0 +1,433 @@ +from pathlib import Path +from typing import Optional + +import accelerate +import torch + +from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base, Diffusers_Config_Base +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.configs.main import Main_GGUF_QwenImage_Config +from invokeai.backend.model_manager.configs.qwen_vl_encoder import ( + QwenVLEncoder_Checkpoint_Config, + QwenVLEncoder_Diffusers_Config, +) +from invokeai.backend.model_manager.load.load_default import ModelLoader +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader +from invokeai.backend.model_manager.taxonomy import ( + AnyModel, + BaseModelType, + ModelFormat, + ModelType, + QwenImageVariantType, + SubModelType, +) +from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor +from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader +from invokeai.backend.util.devices import TorchDevice + + +@ModelLoaderRegistry.register(base=BaseModelType.QwenImage, type=ModelType.Main, format=ModelFormat.Diffusers) +class QwenImageDiffusersModel(GenericDiffusersLoader): + """Class to load Qwen Image Edit main models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if isinstance(config, Checkpoint_Config_Base): + raise NotImplementedError("CheckpointConfigBase is not implemented for Qwen Image Edit models.") + + if submodel_type is None: + raise Exception("A submodel type must be provided when loading main pipelines.") + + model_path = Path(config.path) + load_class = self.get_hf_load_class(model_path, submodel_type) + repo_variant = config.repo_variant if isinstance(config, Diffusers_Config_Base) else None + variant = repo_variant.value if repo_variant else None + model_path = model_path / submodel_type.value + + # We force bfloat16 for Qwen Image Edit models. + # Use `dtype` (newer) with fallback to `torch_dtype` (older diffusers). + dtype_kwarg = {"dtype": torch.bfloat16} + try: + result: AnyModel = load_class.from_pretrained( + model_path, + **dtype_kwarg, + variant=variant, + local_files_only=True, + ) + except TypeError: + # Older diffusers uses torch_dtype instead of dtype + dtype_kwarg = {"torch_dtype": torch.bfloat16} + result = load_class.from_pretrained( + model_path, + **dtype_kwarg, + variant=variant, + local_files_only=True, + ) + except OSError as e: + if variant and "no file named" in str(e): + result = load_class.from_pretrained(model_path, **dtype_kwarg, local_files_only=True) + else: + raise e + + return result + + +@ModelLoaderRegistry.register(base=BaseModelType.QwenImage, type=ModelType.Main, format=ModelFormat.GGUFQuantized) +class QwenImageGGUFCheckpointModel(ModelLoader): + """Class to load GGUF-quantized Qwen Image Edit transformer models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, Checkpoint_Config_Base): + raise ValueError("Only CheckpointConfigBase models are currently supported here.") + + match submodel_type: + case SubModelType.Transformer: + return self._load_from_singlefile(config) + + raise ValueError( + f"Only Transformer submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}" + ) + + def _load_from_singlefile(self, config: AnyModelConfig) -> AnyModel: + from diffusers import QwenImageTransformer2DModel + + if not isinstance(config, Main_GGUF_QwenImage_Config): + raise TypeError(f"Expected Main_GGUF_QwenImage_Config, got {type(config).__name__}.") + model_path = Path(config.path) + + target_device = TorchDevice.choose_torch_device() + compute_dtype = TorchDevice.choose_bfloat16_safe_dtype(target_device) + + sd = gguf_sd_loader(model_path, compute_dtype=compute_dtype) + + # Strip ComfyUI-style prefixes if present + prefix_to_strip = None + for prefix in ["model.diffusion_model.", "diffusion_model."]: + if any(k.startswith(prefix) for k in sd.keys() if isinstance(k, str)): + prefix_to_strip = prefix + break + + if prefix_to_strip: + stripped_sd = {} + for key, value in sd.items(): + if isinstance(key, str) and key.startswith(prefix_to_strip): + stripped_sd[key[len(prefix_to_strip) :]] = value + else: + stripped_sd[key] = value + sd = stripped_sd + + # Auto-detect architecture from state dict + num_layers = 0 + for key in sd.keys(): + if isinstance(key, str) and key.startswith("transformer_blocks."): + parts = key.split(".") + if len(parts) >= 2: + try: + layer_idx = int(parts[1]) + num_layers = max(num_layers, layer_idx + 1) + except ValueError: + pass + + # Detect dimensions from weights + num_attention_heads = 24 # default + attention_head_dim = 128 # default + + if "img_in.weight" in sd: + w = sd["img_in.weight"] + shape = w.tensor_shape if isinstance(w, GGMLTensor) else w.shape + hidden_dim = shape[0] + in_channels = shape[1] + num_attention_heads = hidden_dim // attention_head_dim + + joint_attention_dim = 3584 # default + if "txt_in.weight" in sd: + w = sd["txt_in.weight"] + shape = w.tensor_shape if isinstance(w, GGMLTensor) else w.shape + joint_attention_dim = shape[1] + + model_config: dict = { + "patch_size": 2, + "in_channels": in_channels if "img_in.weight" in sd else 64, + "out_channels": 16, + "num_layers": num_layers if num_layers > 0 else 60, + "attention_head_dim": attention_head_dim, + "num_attention_heads": num_attention_heads, + "joint_attention_dim": joint_attention_dim, + "guidance_embeds": False, + "axes_dims_rope": (16, 56, 56), + } + + # zero_cond_t is only used by edit-variant models. It enables dual modulation + # for noisy vs reference patches. Setting it on txt2img models produces garbage. + # Also requires diffusers 0.37+ (the parameter doesn't exist in older versions). + import inspect + + is_edit = getattr(config, "variant", None) == QwenImageVariantType.Edit + if is_edit and "zero_cond_t" in inspect.signature(QwenImageTransformer2DModel.__init__).parameters: + model_config["zero_cond_t"] = True + + with accelerate.init_empty_weights(): + model = QwenImageTransformer2DModel(**model_config) + + model.load_state_dict(sd, strict=False, assign=True) + return model + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.QwenVLEncoder, format=ModelFormat.QwenVLEncoder) +class QwenVLEncoderLoader(ModelLoader): + """Loads a standalone Qwen2.5-VL encoder (text_encoder/ + tokenizer/ + processor/).""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, QwenVLEncoder_Diffusers_Config): + raise TypeError(f"Expected QwenVLEncoder_Diffusers_Config, got {type(config).__name__}.") + + from transformers import AutoTokenizer, Qwen2_5_VLForConditionalGeneration + + model_path = Path(config.path) + + target_device = TorchDevice.choose_torch_device() + model_dtype = TorchDevice.choose_bfloat16_safe_dtype(target_device) + + match submodel_type: + case SubModelType.Tokenizer: + tokenizer_path = model_path / "tokenizer" + return AutoTokenizer.from_pretrained(str(tokenizer_path), local_files_only=True) + case SubModelType.TextEncoder: + encoder_path = model_path / "text_encoder" + return Qwen2_5_VLForConditionalGeneration.from_pretrained( + str(encoder_path), + torch_dtype=model_dtype, + low_cpu_mem_usage=True, + local_files_only=True, + ) + + raise ValueError( + f"Only Tokenizer and TextEncoder submodels are supported. " + f"Received: {submodel_type.value if submodel_type else 'None'}" + ) + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.QwenVLEncoder, format=ModelFormat.Checkpoint) +class QwenVLEncoderCheckpointLoader(ModelLoader): + """Loads a single-file Qwen2.5-VL encoder checkpoint (e.g. ComfyUI fp8_scaled). + + The checkpoint bundles the language model and the visual tower into one + safetensors file. Tokenizer + processor are pulled from HuggingFace + (`Qwen/Qwen2.5-VL-7B-Instruct`) on first use, with offline cache fallback. + """ + + DEFAULT_HF_REPO = "Qwen/Qwen2.5-VL-7B-Instruct" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, QwenVLEncoder_Checkpoint_Config): + raise TypeError(f"Expected QwenVLEncoder_Checkpoint_Config, got {type(config).__name__}.") + + match submodel_type: + case SubModelType.Tokenizer: + return self._load_tokenizer_with_offline_fallback() + case SubModelType.TextEncoder: + return self._load_text_encoder_from_singlefile(config) + + raise ValueError( + f"Only Tokenizer and TextEncoder submodels are supported. " + f"Received: {submodel_type.value if submodel_type else 'None'}" + ) + + def _load_tokenizer_with_offline_fallback(self) -> AnyModel: + from transformers import AutoTokenizer + + from invokeai.backend.util.logging import InvokeAILogger + + logger = InvokeAILogger.get_logger(self.__class__.__name__) + + try: + return AutoTokenizer.from_pretrained(self.DEFAULT_HF_REPO, local_files_only=True) + except OSError: + logger.info( + f"Tokenizer for single-file Qwen VL encoder not found in HuggingFace cache; " + f"downloading from {self.DEFAULT_HF_REPO} (one-time, requires network access)." + ) + try: + return AutoTokenizer.from_pretrained(self.DEFAULT_HF_REPO) + except OSError as e: + raise RuntimeError( + f"Failed to load Qwen VL tokenizer. Single-file Qwen VL encoder checkpoints do not " + f"include the tokenizer; it must be downloaded from HuggingFace ({self.DEFAULT_HF_REPO}) " + f"on first use. Either restore network access, or install the encoder in the " + f"diffusers folder layout (text_encoder/ + tokenizer/) instead. Original error: {e}" + ) from e + + def _load_text_encoder_from_singlefile(self, config: QwenVLEncoder_Checkpoint_Config) -> AnyModel: + import re + + from safetensors.torch import load_file + from transformers import AutoConfig, Qwen2_5_VLForConditionalGeneration + + from invokeai.backend.util.logging import InvokeAILogger + + logger = InvokeAILogger.get_logger(self.__class__.__name__) + + model_path = Path(config.path) + + target_device = TorchDevice.choose_torch_device() + model_dtype = TorchDevice.choose_bfloat16_safe_dtype(target_device) + + sd = load_file(str(model_path)) + + # Dequantize ComfyUI-style fp8 weights. Two key naming schemes are in the wild: + # - `.weight` + `.weight_scale` (FLUX, Z-Image style) + # - `.weight` + `.scale_weight` (Qwen2.5-VL fp8_scaled style, also + # emits `.scale_input` for activation scaling that we discard). + scale_suffixes = (".weight_scale", ".scale_weight") + weight_scale_keys = [k for k in sd.keys() if isinstance(k, str) and k.endswith(scale_suffixes)] + dequantized_count = 0 + for scale_key in weight_scale_keys: + for suffix in scale_suffixes: + if scale_key.endswith(suffix): + weight_key = scale_key[: -len(suffix)] + ".weight" + break + if weight_key not in sd: + continue + weight = sd[weight_key] + scale = sd[scale_key] + weight_float = weight.float() + scale_float = scale.float() + if scale_float.shape != weight_float.shape and scale_float.numel() > 1: + # Block-wise quantization: expand scale along mismatching dim + for dim in range(len(weight_float.shape)): + if dim < len(scale_float.shape) and scale_float.shape[dim] != weight_float.shape[dim]: + block_size = weight_float.shape[dim] // scale_float.shape[dim] + if block_size > 1: + scale_float = scale_float.repeat_interleave(block_size, dim=dim) + sd[weight_key] = weight_float * scale_float + dequantized_count += 1 + + if dequantized_count > 0: + logger.info(f"Dequantized {dequantized_count} ComfyUI-quantized weights") + + # Strip ComfyUI quantization metadata. `scale_input` is the activation scale used + # at runtime by ComfyUI's fp8 matmul kernels — we run the encoder in bf16 after + # dequantization, so it is not needed. + keys_to_drop = [ + k + for k in sd.keys() + if isinstance(k, str) + and ( + k.endswith(".weight_scale") + or k.endswith(".scale_weight") + or k.endswith(".scale_input") + or "comfy_quant" in k + or k == "scaled_fp8" + ) + ] + for k in keys_to_drop: + del sd[k] + + # ComfyUI single-file checkpoints use the legacy Qwen2.5-VL key layout + # (`visual.X`, `model.X`); transformers ≥4.50 expects `model.visual.X` and + # `model.language_model.X`. Apply the same conversion mapping that + # `Qwen2_5_VLForConditionalGeneration.from_pretrained` would, since + # `load_state_dict` does not. + key_mapping = Qwen2_5_VLForConditionalGeneration._checkpoint_conversion_mapping + if key_mapping: + remapped_sd: dict[str, torch.Tensor] = {} + for old_key, tensor in sd.items(): + new_key = old_key + for pattern, replacement in key_mapping.items(): + new_key, n_replace = re.subn(pattern, replacement, new_key) + if n_replace > 0: + break + remapped_sd[new_key] = tensor + sd = remapped_sd + + # Cast to compute dtype (skip integer/index tensors) + for k in list(sd.keys()): + if sd[k].is_floating_point(): + sd[k] = sd[k].to(model_dtype) + + # Fetch the architecture config from HuggingFace (small, ~5KB). + # Offline fallback: tries cache first, downloads only if missing. + try: + qwen_config = AutoConfig.from_pretrained(self.DEFAULT_HF_REPO, local_files_only=True) + except OSError: + logger.info( + f"Architecture config for single-file Qwen VL encoder not found in HuggingFace cache; " + f"downloading from {self.DEFAULT_HF_REPO} (one-time, ~5KB, requires network access)." + ) + try: + qwen_config = AutoConfig.from_pretrained(self.DEFAULT_HF_REPO) + except OSError as e: + raise RuntimeError( + f"Failed to load Qwen VL architecture config. Single-file Qwen VL encoder checkpoints " + f"do not include the model config; it must be downloaded from HuggingFace " + f"({self.DEFAULT_HF_REPO}) on first use. Either restore network access, or install the " + f"encoder in the diffusers folder layout (text_encoder/config.json + tokenizer/) " + f"instead. Original error: {e}" + ) from e + qwen_config.torch_dtype = model_dtype + + new_sd_size = sum(t.nelement() * t.element_size() for t in sd.values()) + self._ram_cache.make_room(new_sd_size) + + with accelerate.init_empty_weights(): + model = Qwen2_5_VLForConditionalGeneration(qwen_config) + + # Load weights; allow missing keys for tied lm_head and re-initialised buffers. + load_result = model.load_state_dict(sd, strict=False, assign=True) + if load_result.unexpected_keys: + logger.warning( + f"{len(load_result.unexpected_keys)} unexpected keys in checkpoint, " + f"first 5: {load_result.unexpected_keys[:5]}" + ) + + # Tie lm_head ↔ embed_tokens if config requires it and lm_head wasn't loaded + if getattr(qwen_config, "tie_word_embeddings", False): + try: + if hasattr(model, "lm_head") and model.lm_head.weight.is_meta: + model.lm_head.weight = model.model.embed_tokens.weight + else: + model.tie_weights() + except AttributeError: + model.tie_weights() + + # Re-initialise any leftover meta buffers (RoPE inv_freq etc.) + for name, buffer in list(model.named_buffers()): + if not buffer.is_meta: + continue + parts = name.rsplit(".", 1) + if len(parts) == 2: + parent = model.get_submodule(parts[0]) + buffer_name = parts[1] + else: + parent = model + buffer_name = name + # Replace meta buffer with a real (zero) tensor of the same shape; the model + # will recompute or refill these as needed at first forward pass. + try: + shape = buffer.shape + parent.register_buffer(buffer_name, torch.zeros(shape, dtype=model_dtype), persistent=False) + except Exception: + logger.warning(f"Could not re-initialise meta buffer {name}") + + meta_params = [name for name, p in model.named_parameters() if p.is_meta] + if meta_params: + raise RuntimeError(f"Failed to load all parameters from checkpoint. Meta tensors remain: {meta_params[:5]}") + + model.eval() + return model diff --git a/invokeai/backend/model_manager/load/model_loaders/sig_lip.py b/invokeai/backend/model_manager/load/model_loaders/sig_lip.py new file mode 100644 index 00000000000..16b8e6c88da --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/sig_lip.py @@ -0,0 +1,26 @@ +from pathlib import Path +from typing import Optional + +from transformers import SiglipVisionModel + +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.load.load_default import ModelLoader +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.taxonomy import AnyModel, BaseModelType, ModelFormat, ModelType, SubModelType + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.SigLIP, format=ModelFormat.Diffusers) +class SigLIPModelLoader(ModelLoader): + """Class for loading SigLIP models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if submodel_type is not None: + raise ValueError("Unexpected submodel requested for LLaVA OneVision model.") + + model_path = Path(config.path) + model = SiglipVisionModel.from_pretrained(model_path, local_files_only=True, torch_dtype=self._torch_dtype) + return model diff --git a/invokeai/backend/model_manager/load/model_loaders/spandrel_image_to_image.py b/invokeai/backend/model_manager/load/model_loaders/spandrel_image_to_image.py new file mode 100644 index 00000000000..e6d8f429904 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/spandrel_image_to_image.py @@ -0,0 +1,39 @@ +from pathlib import Path +from typing import Optional + +import torch + +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.load.load_default import ModelLoader +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.taxonomy import AnyModel, BaseModelType, ModelFormat, ModelType, SubModelType +from invokeai.backend.spandrel_image_to_image_model import SpandrelImageToImageModel + + +@ModelLoaderRegistry.register( + base=BaseModelType.Any, type=ModelType.SpandrelImageToImage, format=ModelFormat.Checkpoint +) +class SpandrelImageToImageModelLoader(ModelLoader): + """Class for loading Spandrel Image-to-Image models (i.e. models wrapped by spandrel.ImageModelDescriptor).""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if submodel_type is not None: + raise ValueError("Unexpected submodel requested for Spandrel model.") + + model_path = Path(config.path) + model = SpandrelImageToImageModel.load_from_file(model_path) + + torch_dtype = self._torch_dtype + if not model.supports_dtype(torch_dtype): + self._logger.warning( + f"The configured dtype ('{self._torch_dtype}') is not supported by the {model.get_model_type_name()} " + "model. Falling back to 'float32'." + ) + torch_dtype = torch.float32 + model.to(dtype=torch_dtype) + + return model diff --git a/invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py b/invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py new file mode 100644 index 00000000000..d19d6477626 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py @@ -0,0 +1,160 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for StableDiffusion model loading in InvokeAI.""" + +from pathlib import Path +from typing import Optional + +from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion import StableDiffusionPipeline +from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_inpaint import StableDiffusionInpaintPipeline +from diffusers.pipelines.stable_diffusion_xl.pipeline_stable_diffusion_xl import StableDiffusionXLPipeline +from diffusers.pipelines.stable_diffusion_xl.pipeline_stable_diffusion_xl_inpaint import ( + StableDiffusionXLInpaintPipeline, +) + +from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base, Diffusers_Config_Base +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.configs.main import ( + Main_Checkpoint_SD1_Config, + Main_Checkpoint_SD2_Config, + Main_Checkpoint_SDXL_Config, + Main_Checkpoint_SDXLRefiner_Config, + Main_Diffusers_SD1_Config, + Main_Diffusers_SD2_Config, + Main_Diffusers_SDXL_Config, + Main_Diffusers_SDXLRefiner_Config, +) +from invokeai.backend.model_manager.load.model_cache.model_cache import get_model_cache_key +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader +from invokeai.backend.model_manager.taxonomy import ( + AnyModel, + BaseModelType, + ModelFormat, + ModelType, + ModelVariantType, + SubModelType, +) +from invokeai.backend.util.silence_warnings import SilenceWarnings + +VARIANT_TO_IN_CHANNEL_MAP = { + ModelVariantType.Normal: 4, + ModelVariantType.Depth: 5, + ModelVariantType.Inpaint: 9, +} + + +@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusion1, type=ModelType.Main, format=ModelFormat.Diffusers) +@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusion2, type=ModelType.Main, format=ModelFormat.Diffusers) +@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusionXL, type=ModelType.Main, format=ModelFormat.Diffusers) +@ModelLoaderRegistry.register( + base=BaseModelType.StableDiffusionXLRefiner, type=ModelType.Main, format=ModelFormat.Diffusers +) +@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusion3, type=ModelType.Main, format=ModelFormat.Diffusers) +@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusion1, type=ModelType.Main, format=ModelFormat.Checkpoint) +@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusion2, type=ModelType.Main, format=ModelFormat.Checkpoint) +@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusionXL, type=ModelType.Main, format=ModelFormat.Checkpoint) +@ModelLoaderRegistry.register( + base=BaseModelType.StableDiffusionXLRefiner, type=ModelType.Main, format=ModelFormat.Checkpoint +) +class StableDiffusionDiffusersModel(GenericDiffusersLoader): + """Class to load main models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if isinstance(config, Checkpoint_Config_Base): + return self._load_from_singlefile(config, submodel_type) + + if submodel_type is None: + raise Exception("A submodel type must be provided when loading main pipelines.") + + model_path = Path(config.path) + load_class = self.get_hf_load_class(model_path, submodel_type) + repo_variant = config.repo_variant if isinstance(config, Diffusers_Config_Base) else None + variant = repo_variant.value if repo_variant else None + model_path = model_path / submodel_type.value + try: + result: AnyModel = load_class.from_pretrained( + model_path, + torch_dtype=self._torch_dtype, + variant=variant, + local_files_only=True, + ) + except OSError as e: + if variant and "no file named" in str( + e + ): # try without the variant, just in case user's preferences changed + result = load_class.from_pretrained(model_path, torch_dtype=self._torch_dtype, local_files_only=True) + else: + raise e + + result = self._apply_fp8_layerwise_casting(result, config, submodel_type) + return result + + def _load_from_singlefile( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + load_classes = { + BaseModelType.StableDiffusion1: { + ModelVariantType.Normal: StableDiffusionPipeline, + ModelVariantType.Inpaint: StableDiffusionInpaintPipeline, + }, + BaseModelType.StableDiffusion2: { + ModelVariantType.Normal: StableDiffusionPipeline, + ModelVariantType.Inpaint: StableDiffusionInpaintPipeline, + }, + BaseModelType.StableDiffusionXL: { + ModelVariantType.Normal: StableDiffusionXLPipeline, + ModelVariantType.Inpaint: StableDiffusionXLInpaintPipeline, + }, + BaseModelType.StableDiffusionXLRefiner: { + ModelVariantType.Normal: StableDiffusionXLPipeline, + }, + } + assert isinstance( + config, + ( + Main_Diffusers_SD1_Config, + Main_Diffusers_SD2_Config, + Main_Diffusers_SDXL_Config, + Main_Diffusers_SDXLRefiner_Config, + Main_Checkpoint_SD1_Config, + Main_Checkpoint_SD2_Config, + Main_Checkpoint_SDXL_Config, + Main_Checkpoint_SDXLRefiner_Config, + ), + ) + try: + load_class = load_classes[config.base][config.variant] + except KeyError as e: + raise Exception(f"No diffusers pipeline known for base={config.base}, variant={config.variant}") from e + + # Without SilenceWarnings we get log messages like this: + # site-packages/huggingface_hub/file_download.py:1132: FutureWarning: `resume_download` is deprecated and will be removed in version 1.0.0. Downloads always resume when possible. If you want to force a new download, use `force_download=True`. + # warnings.warn( + # Some weights of the model checkpoint were not used when initializing CLIPTextModel: + # ['text_model.embeddings.position_ids'] + # Some weights of the model checkpoint were not used when initializing CLIPTextModelWithProjection: + # ['text_model.embeddings.position_ids'] + + with SilenceWarnings(): + pipeline = load_class.from_single_file(config.path, torch_dtype=self._torch_dtype) + + if not submodel_type: + return pipeline + + # Proactively load the various submodels into the RAM cache so that we don't have to re-load + # the entire pipeline every time a new submodel is needed. + for subtype in SubModelType: + if subtype == submodel_type: + continue + if submodel := getattr(pipeline, subtype.value, None): + self._apply_fp8_layerwise_casting(submodel, config, subtype) + self._ram_cache.put(get_model_cache_key(config.key, subtype), model=submodel) + result = getattr(pipeline, submodel_type.value) + result = self._apply_fp8_layerwise_casting(result, config, submodel_type) + return result diff --git a/invokeai/backend/model_manager/load/model_loaders/text_llm.py b/invokeai/backend/model_manager/load/model_loaders/text_llm.py new file mode 100644 index 00000000000..0ebfe3cc453 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/text_llm.py @@ -0,0 +1,32 @@ +from pathlib import Path +from typing import Optional + +import torch +from transformers import AutoModelForCausalLM + +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.load.load_default import ModelLoader +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.taxonomy import AnyModel, BaseModelType, ModelFormat, ModelType, SubModelType + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.TextLLM, format=ModelFormat.Diffusers) +class TextLLMModelLoader(ModelLoader): + """Class for loading text causal language models (Llama, Phi, Qwen, Mistral, etc.).""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if submodel_type is not None: + raise ValueError("Unexpected submodel requested for TextLLM model.") + + # Use float32 for CPU-only models since CPU fp16 is emulated and slow. + dtype = self._torch_dtype + if getattr(config, "cpu_only", False) is True: + dtype = torch.float32 + + model_path = Path(config.path) + model = AutoModelForCausalLM.from_pretrained(model_path, local_files_only=True, torch_dtype=dtype) + return model diff --git a/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py b/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py new file mode 100644 index 00000000000..2d0411a8df2 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py @@ -0,0 +1,52 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for TI model loading in InvokeAI.""" + +from pathlib import Path +from typing import Optional + +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.load.load_default import ModelLoader +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.taxonomy import ( + AnyModel, + BaseModelType, + ModelFormat, + ModelType, + SubModelType, +) +from invokeai.backend.textual_inversion import TextualInversionModelRaw + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.TextualInversion, format=ModelFormat.EmbeddingFile) +@ModelLoaderRegistry.register( + base=BaseModelType.Any, type=ModelType.TextualInversion, format=ModelFormat.EmbeddingFolder +) +class TextualInversionLoader(ModelLoader): + """Class to load TI models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if submodel_type is not None: + raise ValueError("There are no submodels in a TI model.") + model = TextualInversionModelRaw.from_checkpoint( + file_path=config.path, + dtype=self._torch_dtype, + ) + return model + + # override + def _get_model_path(self, config: AnyModelConfig) -> Path: + model_path = self._app_config.models_path / config.path + + if config.format == ModelFormat.EmbeddingFolder: + path = model_path / "learned_embeds.bin" + else: + path = model_path + + if not path.exists(): + raise OSError(f"The embedding file at {path} was not found") + + return path diff --git a/invokeai/backend/model_manager/load/model_loaders/vae.py b/invokeai/backend/model_manager/load/model_loaders/vae.py new file mode 100644 index 00000000000..720821f3af8 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/vae.py @@ -0,0 +1,80 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for VAE model loading in InvokeAI.""" + +from typing import Optional + +from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL + +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.configs.vae import ( + VAE_Checkpoint_Anima_Config, + VAE_Checkpoint_Config_Base, + VAE_Checkpoint_QwenImage_Config, +) +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader +from invokeai.backend.model_manager.taxonomy import ( + AnyModel, + BaseModelType, + ModelFormat, + ModelType, + SubModelType, +) + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.VAE, format=ModelFormat.Diffusers) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.VAE, format=ModelFormat.Checkpoint) +class VAELoader(GenericDiffusersLoader): + """Class to load VAE models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if isinstance(config, VAE_Checkpoint_Anima_Config): + from diffusers.models.autoencoders import AutoencoderKLWan + + return AutoencoderKLWan.from_single_file( + config.path, + torch_dtype=self._torch_dtype, + ) + elif isinstance(config, VAE_Checkpoint_QwenImage_Config): + return self._load_qwen_image_vae(config) + elif isinstance(config, VAE_Checkpoint_Config_Base): + return AutoencoderKL.from_single_file( + config.path, + torch_dtype=self._torch_dtype, + ) + else: + return super()._load_model(config, submodel_type) + + def _load_qwen_image_vae(self, config: VAE_Checkpoint_QwenImage_Config) -> AnyModel: + """Load a Qwen Image VAE from a single safetensors file. + + The Qwen Image VAE checkpoint is expected to be in the diffusers state-dict + layout (i.e. the same keys as `vae/diffusion_pytorch_model.safetensors` from + the Qwen-Image repo). `AutoencoderKLQwenImage` does not register a single-file + conversion in diffusers, so we instantiate the model with default config and + load the state dict directly. + """ + import accelerate + from diffusers.models.autoencoders.autoencoder_kl_qwenimage import AutoencoderKLQwenImage + from safetensors.torch import load_file + + sd = load_file(config.path) + + if self._torch_dtype is not None: + for k in list(sd.keys()): + if sd[k].is_floating_point(): + sd[k] = sd[k].to(self._torch_dtype) + + new_sd_size = sum(t.nelement() * t.element_size() for t in sd.values()) + self._ram_cache.make_room(new_sd_size) + + with accelerate.init_empty_weights(): + model = AutoencoderKLQwenImage() + + model.load_state_dict(sd, strict=True, assign=True) + model.eval() + return model diff --git a/invokeai/backend/model_manager/load/model_loaders/z_image.py b/invokeai/backend/model_manager/load/model_loaders/z_image.py new file mode 100644 index 00000000000..6c2102933af --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/z_image.py @@ -0,0 +1,1082 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for Z-Image model loading in InvokeAI.""" + +from pathlib import Path +from typing import Any, Optional + +import accelerate +import torch +from transformers import AutoTokenizer, Qwen3ForCausalLM + +from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base, Diffusers_Config_Base +from invokeai.backend.model_manager.configs.controlnet import ControlNet_Checkpoint_ZImage_Config +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.configs.main import Main_Checkpoint_ZImage_Config, Main_GGUF_ZImage_Config +from invokeai.backend.model_manager.configs.qwen3_encoder import ( + Qwen3Encoder_Checkpoint_Config, + Qwen3Encoder_GGUF_Config, + Qwen3Encoder_Qwen3Encoder_Config, +) +from invokeai.backend.model_manager.load.load_default import ModelLoader +from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry +from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader +from invokeai.backend.model_manager.taxonomy import ( + AnyModel, + BaseModelType, + ModelFormat, + ModelType, + SubModelType, +) +from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader +from invokeai.backend.util.devices import TorchDevice + + +def _convert_z_image_gguf_to_diffusers(sd: dict[str, Any]) -> dict[str, Any]: + """Convert Z-Image GGUF state dict keys to diffusers format. + + The GGUF format uses original model keys that differ from diffusers: + - qkv.weight (fused) -> to_q.weight, to_k.weight, to_v.weight (split) + - out.weight -> to_out.0.weight + - q_norm.weight -> norm_q.weight + - k_norm.weight -> norm_k.weight + - x_embedder.* -> all_x_embedder.2-1.* + - final_layer.* -> all_final_layer.2-1.* + - norm_final.* -> skipped (diffusers uses non-learnable LayerNorm) + - x_pad_token, cap_pad_token: [dim] -> [1, dim] (diffusers expects batch dimension) + """ + new_sd: dict[str, Any] = {} + + for key, value in sd.items(): + if not isinstance(key, str): + new_sd[key] = value + continue + + # Handle padding tokens: GGUF has shape [dim], diffusers expects [1, dim] + if key in ("x_pad_token", "cap_pad_token"): + if hasattr(value, "shape") and len(value.shape) == 1: + # GGMLTensor doesn't support unsqueeze, so dequantize first if needed + if hasattr(value, "get_dequantized_tensor"): + value = value.get_dequantized_tensor() + # Use reshape instead of unsqueeze for better compatibility + value = torch.as_tensor(value).reshape(1, -1) + new_sd[key] = value + continue + + # Handle x_embedder -> all_x_embedder.2-1 + if key.startswith("x_embedder."): + suffix = key[len("x_embedder.") :] + new_key = f"all_x_embedder.2-1.{suffix}" + new_sd[new_key] = value + continue + + # Handle final_layer -> all_final_layer.2-1 + if key.startswith("final_layer."): + suffix = key[len("final_layer.") :] + new_key = f"all_final_layer.2-1.{suffix}" + new_sd[new_key] = value + continue + + # Skip norm_final keys - the diffusers model uses LayerNorm with elementwise_affine=False + # (no learnable weight/bias), but some checkpoints (e.g., FP8) include these as all-zeros + if key.startswith("norm_final."): + continue + + # Handle fused QKV weights - need to split + if ".attention.qkv." in key: + # Get the layer prefix and suffix + prefix = key.rsplit(".attention.qkv.", 1)[0] + suffix = key.rsplit(".attention.qkv.", 1)[1] # "weight" or "bias" + + # Skip non-weight/bias tensors (e.g., FP8 scale_weight tensors) + # These are quantization metadata and should not be split + if suffix not in ("weight", "bias"): + new_sd[key] = value + continue + + # Split the fused QKV tensor into Q, K, V + tensor = value + if hasattr(tensor, "shape"): + if tensor.shape[0] % 3 != 0: + raise ValueError( + f"Cannot split QKV tensor '{key}': first dimension ({tensor.shape[0]}) " + "is not divisible by 3. The model file may be corrupted or incompatible." + ) + dim = tensor.shape[0] // 3 + q = tensor[:dim] + k = tensor[dim : 2 * dim] + v = tensor[2 * dim :] + + new_sd[f"{prefix}.attention.to_q.{suffix}"] = q + new_sd[f"{prefix}.attention.to_k.{suffix}"] = k + new_sd[f"{prefix}.attention.to_v.{suffix}"] = v + continue + + # Handle attention key renaming + if ".attention." in key: + new_key = key.replace(".q_norm.", ".norm_q.") + new_key = new_key.replace(".k_norm.", ".norm_k.") + new_key = new_key.replace(".attention.out.", ".attention.to_out.0.") + new_sd[new_key] = value + continue + + # For all other keys, just copy as-is + new_sd[key] = value + + return new_sd + + +@ModelLoaderRegistry.register(base=BaseModelType.ZImage, type=ModelType.Main, format=ModelFormat.Diffusers) +class ZImageDiffusersModel(GenericDiffusersLoader): + """Class to load Z-Image main models (Z-Image-Turbo, Z-Image-Base, Z-Image-Edit).""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if isinstance(config, Checkpoint_Config_Base): + raise NotImplementedError("CheckpointConfigBase is not implemented for Z-Image models.") + + if submodel_type is None: + raise Exception("A submodel type must be provided when loading main pipelines.") + + model_path = Path(config.path) + load_class = self.get_hf_load_class(model_path, submodel_type) + repo_variant = config.repo_variant if isinstance(config, Diffusers_Config_Base) else None + variant = repo_variant.value if repo_variant else None + model_path = model_path / submodel_type.value + + # Z-Image prefers bfloat16, but use safe dtype based on target device capabilities. + target_device = TorchDevice.choose_torch_device() + dtype = TorchDevice.choose_bfloat16_safe_dtype(target_device) + try: + result: AnyModel = load_class.from_pretrained( + model_path, + torch_dtype=dtype, + variant=variant, + ) + except OSError as e: + if variant and "no file named" in str( + e + ): # try without the variant, just in case user's preferences changed + result = load_class.from_pretrained(model_path, torch_dtype=dtype) + else: + raise e + + result = self._apply_fp8_layerwise_casting(result, config, submodel_type) + return result + + +@ModelLoaderRegistry.register(base=BaseModelType.ZImage, type=ModelType.Main, format=ModelFormat.Checkpoint) +class ZImageCheckpointModel(ModelLoader): + """Class to load Z-Image transformer models from single-file checkpoints (safetensors, etc).""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, Checkpoint_Config_Base): + raise ValueError("Only CheckpointConfigBase models are currently supported here.") + + match submodel_type: + case SubModelType.Transformer: + return self._load_from_singlefile(config) + + raise ValueError( + f"Only Transformer submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}" + ) + + def _load_from_singlefile( + self, + config: AnyModelConfig, + ) -> AnyModel: + from diffusers import ZImageTransformer2DModel + from safetensors.torch import load_file + + if not isinstance(config, Main_Checkpoint_ZImage_Config): + raise TypeError( + f"Expected Main_Checkpoint_ZImage_Config, got {type(config).__name__}. " + "Model configuration type mismatch." + ) + model_path = Path(config.path) + + # Load the state dict from safetensors/checkpoint file + sd = load_file(model_path) + + # Some Z-Image checkpoint files have keys prefixed with "diffusion_model." or + # "model.diffusion_model." (ComfyUI-style format). Check if we need to strip this prefix. + prefix_to_strip = None + for prefix in ["model.diffusion_model.", "diffusion_model."]: + if any(k.startswith(prefix) for k in sd.keys() if isinstance(k, str)): + prefix_to_strip = prefix + break + + if prefix_to_strip: + stripped_sd = {} + for key, value in sd.items(): + if isinstance(key, str) and key.startswith(prefix_to_strip): + stripped_sd[key[len(prefix_to_strip) :]] = value + else: + stripped_sd[key] = value + sd = stripped_sd + + # Check if the state dict is in original format (not diffusers format) + # Original format has keys like "x_embedder.weight" instead of "all_x_embedder.2-1.weight" + needs_conversion = any(k.startswith("x_embedder.") for k in sd.keys() if isinstance(k, str)) + + if needs_conversion: + # Convert from original format to diffusers format + sd = _convert_z_image_gguf_to_diffusers(sd) + + # Create an empty model with the default Z-Image config + # Z-Image-Turbo uses these default parameters from diffusers + with accelerate.init_empty_weights(): + model = ZImageTransformer2DModel( + all_patch_size=(2,), + all_f_patch_size=(1,), + in_channels=16, + dim=3840, + n_layers=30, + n_refiner_layers=2, + n_heads=30, + n_kv_heads=30, + norm_eps=1e-05, + qk_norm=True, + cap_feat_dim=2560, + rope_theta=256.0, + t_scale=1000.0, + axes_dims=[32, 48, 48], + axes_lens=[1024, 512, 512], + ) + + # Determine safe dtype based on target device capabilities + target_device = TorchDevice.choose_torch_device() + model_dtype = TorchDevice.choose_bfloat16_safe_dtype(target_device) + + # Filter out keys that don't belong to the ZImageTransformer2DModel. + # Merged checkpoints (e.g. LoRA-baked models) may bundle text encoder weights + # (text_encoders.*) or other non-transformer keys alongside the transformer weights. + # Also filter FP8 quantization metadata (scale_weight, scaled_fp8). + valid_prefixes = ( + "all_x_embedder.", + "all_final_layer.", + "layers.", + "noise_refiner.", + "context_refiner.", + "t_embedder.", + "cap_embedder.", + "rope_embedder.", + ) + valid_exact = {"x_pad_token", "cap_pad_token"} + keys_to_remove = [ + k + for k in sd.keys() + if not (k.startswith(valid_prefixes) or k in valid_exact) + or k.endswith(".scale_weight") + or k == "scaled_fp8" + ] + for k in keys_to_remove: + del sd[k] + + # Handle memory management and dtype conversion + new_sd_size = sum([ten.nelement() * model_dtype.itemsize for ten in sd.values()]) + self._ram_cache.make_room(new_sd_size) + + # Convert to target dtype + for k in sd.keys(): + sd[k] = sd[k].to(model_dtype) + + model.load_state_dict(sd, assign=True) + return model + + +@ModelLoaderRegistry.register(base=BaseModelType.ZImage, type=ModelType.Main, format=ModelFormat.GGUFQuantized) +class ZImageGGUFCheckpointModel(ModelLoader): + """Class to load GGUF-quantized Z-Image transformer models.""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, Checkpoint_Config_Base): + raise ValueError("Only CheckpointConfigBase models are currently supported here.") + + match submodel_type: + case SubModelType.Transformer: + return self._load_from_singlefile(config) + + raise ValueError( + f"Only Transformer submodels are currently supported. Received: {submodel_type.value if submodel_type else 'None'}" + ) + + def _load_from_singlefile( + self, + config: AnyModelConfig, + ) -> AnyModel: + from diffusers import ZImageTransformer2DModel + + if not isinstance(config, Main_GGUF_ZImage_Config): + raise TypeError( + f"Expected Main_GGUF_ZImage_Config, got {type(config).__name__}. Model configuration type mismatch." + ) + model_path = Path(config.path) + + # Determine safe dtype based on target device capabilities + target_device = TorchDevice.choose_torch_device() + compute_dtype = TorchDevice.choose_bfloat16_safe_dtype(target_device) + + # Load the GGUF state dict + sd = gguf_sd_loader(model_path, compute_dtype=compute_dtype) + + # Some Z-Image GGUF models have keys prefixed with "diffusion_model." or + # "model.diffusion_model." (ComfyUI-style format). Check if we need to strip this prefix. + prefix_to_strip = None + for prefix in ["model.diffusion_model.", "diffusion_model."]: + if any(k.startswith(prefix) for k in sd.keys() if isinstance(k, str)): + prefix_to_strip = prefix + break + + if prefix_to_strip: + stripped_sd = {} + for key, value in sd.items(): + if isinstance(key, str) and key.startswith(prefix_to_strip): + stripped_sd[key[len(prefix_to_strip) :]] = value + else: + stripped_sd[key] = value + sd = stripped_sd + + # Convert GGUF format keys to diffusers format + sd = _convert_z_image_gguf_to_diffusers(sd) + + # Create an empty model with the default Z-Image config + # Z-Image-Turbo uses these default parameters from diffusers + with accelerate.init_empty_weights(): + model = ZImageTransformer2DModel( + all_patch_size=(2,), + all_f_patch_size=(1,), + in_channels=16, + dim=3840, + n_layers=30, + n_refiner_layers=2, + n_heads=30, + n_kv_heads=30, + norm_eps=1e-05, + qk_norm=True, + cap_feat_dim=2560, + rope_theta=256.0, + t_scale=1000.0, + axes_dims=[32, 48, 48], + axes_lens=[1024, 512, 512], + ) + + model.load_state_dict(sd, assign=True) + return model + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.Qwen3Encoder, format=ModelFormat.Qwen3Encoder) +class Qwen3EncoderLoader(ModelLoader): + """Class to load standalone Qwen3 Encoder models for Z-Image (directory format).""" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, Qwen3Encoder_Qwen3Encoder_Config): + raise ValueError("Only Qwen3Encoder_Qwen3Encoder_Config models are supported here.") + + model_path = Path(config.path) + + # Support both structures: + # 1. Full model: model_root/text_encoder/ and model_root/tokenizer/ + # 2. Standalone download: model_root/ contains text_encoder files directly + text_encoder_path = model_path / "text_encoder" + tokenizer_path = model_path / "tokenizer" + + # Check if this is a standalone text_encoder download (no nested text_encoder folder) + is_standalone = not text_encoder_path.exists() and (model_path / "config.json").exists() + + if is_standalone: + text_encoder_path = model_path + tokenizer_path = model_path # Tokenizer files should also be in root + + match submodel_type: + case SubModelType.Tokenizer: + # Use local_files_only=True to prevent network requests for validation + # The tokenizer files should already exist locally in the model directory + return AutoTokenizer.from_pretrained(tokenizer_path, local_files_only=True) + case SubModelType.TextEncoder: + # Determine safe dtype based on target device capabilities + target_device = TorchDevice.choose_torch_device() + model_dtype = TorchDevice.choose_bfloat16_safe_dtype(target_device) + # Use local_files_only=True to prevent network requests for validation + return Qwen3ForCausalLM.from_pretrained( + text_encoder_path, + torch_dtype=model_dtype, + low_cpu_mem_usage=True, + local_files_only=True, + ) + + raise ValueError( + f"Only Tokenizer and TextEncoder submodels are supported. Received: {submodel_type.value if submodel_type else 'None'}" + ) + + +@ModelLoaderRegistry.register(base=BaseModelType.ZImage, type=ModelType.ControlNet, format=ModelFormat.Checkpoint) +class ZImageControlCheckpointModel(ModelLoader): + """Class to load Z-Image Control adapter models from safetensors checkpoint. + + Z-Image Control models are standalone adapters containing control layers + (control_layers, control_all_x_embedder, control_noise_refiner) that can be + combined with a base ZImageTransformer2DModel at runtime for spatial conditioning + (Canny, HED, Depth, Pose, MLSD). + """ + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, Checkpoint_Config_Base): + raise ValueError("Only CheckpointConfigBase models are supported here.") + + # ControlNet type models don't use submodel_type - load the adapter directly + return self._load_control_adapter(config) + + def _load_control_adapter( + self, + config: AnyModelConfig, + ) -> AnyModel: + from safetensors.torch import load_file + + from invokeai.backend.z_image.z_image_control_adapter import ZImageControlAdapter + + assert isinstance(config, ControlNet_Checkpoint_ZImage_Config) + model_path = Path(config.path) + + # Load the safetensors state dict + sd = load_file(model_path) + + # Determine number of control blocks from state dict + # Control blocks are named control_layers.0, control_layers.1, etc. + control_block_indices = set() + for key in sd.keys(): + if key.startswith("control_layers."): + parts = key.split(".") + if len(parts) > 1 and parts[1].isdigit(): + control_block_indices.add(int(parts[1])) + num_control_blocks = len(control_block_indices) if control_block_indices else 6 + + # Determine number of refiner layers from state dict + refiner_indices: set[int] = set() + for key in sd.keys(): + if key.startswith("control_noise_refiner."): + parts = key.split(".") + if len(parts) > 1 and parts[1].isdigit(): + refiner_indices.add(int(parts[1])) + n_refiner_layers = len(refiner_indices) if refiner_indices else 2 + + # Determine control_in_dim from embedder weight shape + # control_in_dim = weight.shape[1] / (f_patch_size * patch_size * patch_size) + # For patch_size=2, f_patch_size=1: control_in_dim = weight.shape[1] / 4 + control_in_dim = 16 # Default for V1 + embedder_key = "control_all_x_embedder.2-1.weight" + if embedder_key in sd: + weight_shape = sd[embedder_key].shape + # weight_shape[1] = f_patch_size * patch_size * patch_size * control_in_dim + control_in_dim = weight_shape[1] // 4 # 4 = 1 * 2 * 2 + + # Log detected configuration for debugging + from invokeai.backend.util.logging import InvokeAILogger + + logger = InvokeAILogger.get_logger(self.__class__.__name__) + version = "V2.0" if control_in_dim > 16 else "V1" + logger.info( + f"Z-Image ControlNet detected: {version} " + f"(control_in_dim={control_in_dim}, num_control_blocks={num_control_blocks}, " + f"n_refiner_layers={n_refiner_layers})" + ) + + # Create an empty control adapter + dim = 3840 + with accelerate.init_empty_weights(): + model = ZImageControlAdapter( + num_control_blocks=num_control_blocks, + control_in_dim=control_in_dim, + all_patch_size=(2,), + all_f_patch_size=(1,), + dim=dim, + n_refiner_layers=n_refiner_layers, + n_heads=30, + n_kv_heads=30, + norm_eps=1e-05, + qk_norm=True, + ) + + # Load state dict with strict=False to handle missing keys like x_pad_token + # Some control adapters may not include x_pad_token in their checkpoint + missing_keys, unexpected_keys = model.load_state_dict(sd, assign=True, strict=False) + + # Initialize x_pad_token if it was missing from the checkpoint + if "x_pad_token" in missing_keys: + import torch.nn as nn + + model.x_pad_token = nn.Parameter(torch.empty(dim)) + nn.init.normal_(model.x_pad_token, std=0.02) + + return model + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.Qwen3Encoder, format=ModelFormat.Checkpoint) +class Qwen3EncoderCheckpointLoader(ModelLoader): + """Class to load single-file Qwen3 Encoder models for Z-Image (safetensors format).""" + + # Default HuggingFace model to load tokenizer from when using single-file Qwen3 encoder + # Must be Qwen3 (not Qwen2.5) to match Z-Image's text encoder architecture and special tokens + DEFAULT_TOKENIZER_SOURCE = "Qwen/Qwen3-4B" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, Qwen3Encoder_Checkpoint_Config): + raise ValueError("Only Qwen3Encoder_Checkpoint_Config models are supported here.") + + match submodel_type: + case SubModelType.TextEncoder: + return self._load_from_singlefile(config) + case SubModelType.Tokenizer: + # For single-file Qwen3, load tokenizer from HuggingFace + # Try local cache first to support offline usage after initial download + return self._load_tokenizer_with_offline_fallback() + + raise ValueError( + f"Only TextEncoder and Tokenizer submodels are supported. Received: {submodel_type.value if submodel_type else 'None'}" + ) + + def _load_tokenizer_with_offline_fallback(self) -> AnyModel: + """Load tokenizer with local_files_only fallback for offline support. + + First tries to load from local cache (offline), falling back to network download + if the tokenizer hasn't been cached yet. This ensures offline operation after + the initial download. + """ + try: + # Try loading from local cache first (supports offline usage) + return AutoTokenizer.from_pretrained(self.DEFAULT_TOKENIZER_SOURCE, local_files_only=True) + except OSError: + # Not in cache yet, download from HuggingFace + return AutoTokenizer.from_pretrained(self.DEFAULT_TOKENIZER_SOURCE) + + def _load_from_singlefile( + self, + config: AnyModelConfig, + ) -> AnyModel: + from safetensors.torch import load_file + from transformers import Qwen3Config, Qwen3ForCausalLM + + from invokeai.backend.util.logging import InvokeAILogger + + logger = InvokeAILogger.get_logger(self.__class__.__name__) + + if not isinstance(config, Qwen3Encoder_Checkpoint_Config): + raise TypeError( + f"Expected Qwen3Encoder_Checkpoint_Config, got {type(config).__name__}. " + "Model configuration type mismatch." + ) + model_path = Path(config.path) + + # Determine safe dtype based on target device capabilities + target_device = TorchDevice.choose_torch_device() + model_dtype = TorchDevice.choose_bfloat16_safe_dtype(target_device) + + # Load the state dict from safetensors file + sd = load_file(model_path) + + # Handle ComfyUI quantized checkpoints + # ComfyUI stores quantized weights with accompanying scale factors: + # - layer.weight: quantized data (FP8) + # - layer.weight_scale: scale factor (FP32 scalar) + # Dequantization formula: dequantized = weight.to(dtype) * weight_scale + # Reference: https://github.com/Comfy-Org/ComfyUI/blob/master/QUANTIZATION.md + original_key_count = len(sd) + weight_scale_keys = [k for k in sd.keys() if k.endswith(".weight_scale")] + dequantized_count = 0 + + for scale_key in weight_scale_keys: + # Get the corresponding weight key (remove "_scale" suffix) + weight_key = scale_key.replace(".weight_scale", ".weight") + if weight_key in sd: + weight = sd[weight_key] + scale = sd[scale_key] + # Dequantize: convert to float and multiply by scale + # Handle block-wise quantization (e.g., FP4 with block_size=8) + # where scale has shape [weight_dim / block_size, ...] + # Note: Float8 types (e.g., float8_e4m3fn) require .float() instead of .to(torch.float32) + # as PyTorch doesn't support direct type promotion for Float8 types + weight_float = weight.float() + scale = scale.float() + if scale.shape != weight_float.shape and scale.numel() > 1: + # Block-wise quantization: need to expand scale to match weight shape + # Find which dimension differs and repeat scale along that dimension + for dim in range(len(weight_float.shape)): + if dim < len(scale.shape) and scale.shape[dim] != weight_float.shape[dim]: + block_size = weight_float.shape[dim] // scale.shape[dim] + if block_size > 1: + # Repeat scale along this dimension to match weight shape + scale = scale.repeat_interleave(block_size, dim=dim) + sd[weight_key] = weight_float * scale + dequantized_count += 1 + + if dequantized_count > 0: + logger.info(f"Dequantized {dequantized_count} ComfyUI quantized weights") + + # Filter out ComfyUI quantization metadata keys (comfy_quant, weight_scale) + # These are no longer needed after dequantization + comfy_metadata_keys = [k for k in sd.keys() if "comfy_quant" in k or "weight_scale" in k] + for k in comfy_metadata_keys: + del sd[k] + if comfy_metadata_keys: + logger.info(f"Filtered out {len(comfy_metadata_keys)} ComfyUI quantization metadata keys") + + logger.info(f"Loaded state dict with {len(sd)} keys (originally {original_key_count})") + + # Count the number of layers by looking at layer keys + layer_count = 0 + for key in sd.keys(): + if isinstance(key, str) and key.startswith("model.layers."): + parts = key.split(".") + if len(parts) > 2: + try: + layer_idx = int(parts[2]) + layer_count = max(layer_count, layer_idx + 1) + except ValueError: + pass + + # Get vocab size from embed_tokens weight shape + embed_weight = sd.get("model.embed_tokens.weight") + if embed_weight is None: + raise ValueError("Could not find model.embed_tokens.weight in state dict") + + vocab_size = embed_weight.shape[0] + embed_hidden_size = embed_weight.shape[1] + + # Detect model variant based on embed_tokens hidden size and layer count + # FLUX 2 Klein / Z-Image uses Qwen3 configurations from ComfyUI: + # Reference: https://github.com/comfyanonymous/ComfyUI/blob/master/comfy/text_encoders/llama.py + # - Qwen3-4B: hidden_size=2560, 36 layers, 32 heads, 8 KV heads, intermediate=9728 + # - Qwen3-8B: hidden_size=4096, 36 layers, 32 heads, 8 KV heads, intermediate=12288 + if embed_hidden_size == 2560 and layer_count == 36: + # Qwen3-4B variant (FLUX 2 Klein / Z-Image) + logger.info("Detected Qwen3-4B variant (FLUX 2 Klein / Z-Image)") + hidden_size = 2560 + num_attention_heads = 32 + num_kv_heads = 8 + intermediate_size = 9728 + head_dim = 128 + max_position_embeddings = 40960 + elif embed_hidden_size == 4096 and layer_count == 36: + # Qwen3-8B variant + logger.info("Detected Qwen3-8B variant") + hidden_size = 4096 + num_attention_heads = 32 + num_kv_heads = 8 + intermediate_size = 12288 + head_dim = 128 + max_position_embeddings = 40960 + else: + # Unknown variant - try to detect from weights + logger.warning( + f"Unknown Qwen3 variant: embed_hidden_size={embed_hidden_size}, layers={layer_count}. " + "Attempting to detect configuration from weights..." + ) + q_proj_weight = sd.get("model.layers.0.self_attn.q_proj.weight") + k_proj_weight = sd.get("model.layers.0.self_attn.k_proj.weight") + gate_proj_weight = sd.get("model.layers.0.mlp.gate_proj.weight") + + if q_proj_weight is None or k_proj_weight is None or gate_proj_weight is None: + raise ValueError("Could not find attention/mlp weights to determine configuration") + + hidden_size = embed_hidden_size + head_dim = 128 + num_attention_heads = q_proj_weight.shape[0] // head_dim + num_kv_heads = k_proj_weight.shape[0] // head_dim + intermediate_size = gate_proj_weight.shape[0] + max_position_embeddings = 40960 + + logger.info( + f"Qwen3 config: hidden_size={hidden_size}, layers={layer_count}, " + f"heads={num_attention_heads}, kv_heads={num_kv_heads}, intermediate={intermediate_size}" + ) + + # Create Qwen3 config + qwen_config = Qwen3Config( + vocab_size=vocab_size, + hidden_size=hidden_size, + intermediate_size=intermediate_size, + num_hidden_layers=layer_count, + num_attention_heads=num_attention_heads, + num_key_value_heads=num_kv_heads, + head_dim=head_dim, + max_position_embeddings=max_position_embeddings, + rms_norm_eps=1e-6, + tie_word_embeddings=True, + rope_theta=1000000.0, + use_sliding_window=False, + attention_bias=False, + attention_dropout=0.0, + torch_dtype=model_dtype, + ) + + # Handle memory management + new_sd_size = sum([ten.nelement() * model_dtype.itemsize for ten in sd.values()]) + self._ram_cache.make_room(new_sd_size) + + # Convert to target dtype + for k in sd.keys(): + sd[k] = sd[k].to(model_dtype) + + # Use Qwen3ForCausalLM - the correct model class for Z-Image text encoder + # Use init_empty_weights for fast model creation, then load weights with assign=True + with accelerate.init_empty_weights(): + model = Qwen3ForCausalLM(qwen_config) + + # Load the text model weights from checkpoint + # assign=True replaces meta tensors with real ones from state dict + model.load_state_dict(sd, strict=False, assign=True) + + # Handle tied weights: lm_head shares weight with embed_tokens when tie_word_embeddings=True + # This doesn't work automatically with init_empty_weights, so we need to manually tie them + if qwen_config.tie_word_embeddings: + model.tie_weights() + + # Re-initialize any remaining meta tensor buffers (like rotary embeddings inv_freq) + # These are computed from config, not loaded from checkpoint + for name, buffer in list(model.named_buffers()): + if buffer.is_meta: + # Get parent module and buffer name + parts = name.rsplit(".", 1) + if len(parts) == 2: + parent = model.get_submodule(parts[0]) + buffer_name = parts[1] + else: + parent = model + buffer_name = name + + # Re-initialize the buffer based on expected shape and dtype + # For rotary embeddings, this is inv_freq which is computed from config + if buffer_name == "inv_freq": + # Compute inv_freq from config (same logic as Qwen3RotaryEmbedding.__init__) + base = qwen_config.rope_theta + inv_freq = 1.0 / (base ** (torch.arange(0, head_dim, 2, dtype=torch.float32) / head_dim)) + parent.register_buffer(buffer_name, inv_freq.to(model_dtype), persistent=False) + else: + # For other buffers, log warning + logger.warning(f"Re-initializing unknown meta buffer: {name}") + + return model + + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.Qwen3Encoder, format=ModelFormat.GGUFQuantized) +class Qwen3EncoderGGUFLoader(ModelLoader): + """Class to load GGUF-quantized Qwen3 Encoder models for Z-Image.""" + + # Default HuggingFace model to load tokenizer from when using GGUF Qwen3 encoder + # Must be Qwen3 (not Qwen2.5) to match Z-Image's text encoder architecture and special tokens + DEFAULT_TOKENIZER_SOURCE = "Qwen/Qwen3-4B" + + def _load_model( + self, + config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not isinstance(config, Qwen3Encoder_GGUF_Config): + raise ValueError("Only Qwen3Encoder_GGUF_Config models are supported here.") + + match submodel_type: + case SubModelType.TextEncoder: + return self._load_from_gguf(config) + case SubModelType.Tokenizer: + # For GGUF Qwen3, load tokenizer from HuggingFace + # Try local cache first to support offline usage after initial download + return self._load_tokenizer_with_offline_fallback() + + raise ValueError( + f"Only TextEncoder and Tokenizer submodels are supported. Received: {submodel_type.value if submodel_type else 'None'}" + ) + + def _load_tokenizer_with_offline_fallback(self) -> AnyModel: + """Load tokenizer with local_files_only fallback for offline support. + + First tries to load from local cache (offline), falling back to network download + if the tokenizer hasn't been cached yet. This ensures offline operation after + the initial download. + """ + try: + # Try loading from local cache first (supports offline usage) + return AutoTokenizer.from_pretrained(self.DEFAULT_TOKENIZER_SOURCE, local_files_only=True) + except OSError: + # Not in cache yet, download from HuggingFace + return AutoTokenizer.from_pretrained(self.DEFAULT_TOKENIZER_SOURCE) + + def _load_from_gguf( + self, + config: AnyModelConfig, + ) -> AnyModel: + from transformers import Qwen3Config, Qwen3ForCausalLM + + from invokeai.backend.util.logging import InvokeAILogger + + logger = InvokeAILogger.get_logger(self.__class__.__name__) + + if not isinstance(config, Qwen3Encoder_GGUF_Config): + raise TypeError( + f"Expected Qwen3Encoder_GGUF_Config, got {type(config).__name__}. Model configuration type mismatch." + ) + model_path = Path(config.path) + + # Determine safe dtype based on target device capabilities + target_device = TorchDevice.choose_torch_device() + compute_dtype = TorchDevice.choose_bfloat16_safe_dtype(target_device) + + # Load the GGUF state dict - this returns GGMLTensor wrappers (on CPU) + # We keep them on CPU and let the model cache system handle GPU movement + # via apply_custom_layers_to_model() and the partial loading cache + sd = gguf_sd_loader(model_path, compute_dtype=compute_dtype) + + # Check if this is llama.cpp format (blk.X.) or PyTorch format (model.layers.X.) + is_llamacpp_format = any(k.startswith("blk.") for k in sd.keys() if isinstance(k, str)) + + if is_llamacpp_format: + logger.info("Detected llama.cpp GGUF format, converting keys to PyTorch format") + sd = self._convert_llamacpp_to_pytorch(sd) + + # Determine Qwen model configuration from state dict + # Count the number of layers by looking at layer keys + layer_count = 0 + for key in sd.keys(): + if isinstance(key, str) and key.startswith("model.layers."): + parts = key.split(".") + if len(parts) > 2: + try: + layer_idx = int(parts[2]) + layer_count = max(layer_count, layer_idx + 1) + except ValueError: + pass + + # Get vocab size from embed_tokens weight shape + embed_weight = sd.get("model.embed_tokens.weight") + if embed_weight is None: + raise ValueError("Could not find model.embed_tokens.weight in state dict") + + # Handle GGMLTensor shape access + embed_shape = embed_weight.shape if hasattr(embed_weight, "shape") else embed_weight.tensor_shape + if len(embed_shape) != 2: + raise ValueError( + f"Expected 2D embed_tokens weight tensor, got shape {embed_shape}. " + "The model file may be corrupted or incompatible." + ) + vocab_size = embed_shape[0] + + # Detect attention configuration from layer weights + # IMPORTANT: Use layer 1 (not layer 0) because some models like FLUX 2 Klein have a special + # first layer with different dimensions (input projection layer) while the rest of the + # transformer layers have a different hidden_size. Using a middle layer ensures we get + # the representative hidden_size for the bulk of the model. + # Fall back to layer 0 if layer 1 doesn't exist. + q_proj_weight = sd.get("model.layers.1.self_attn.q_proj.weight") + k_proj_weight = sd.get("model.layers.1.self_attn.k_proj.weight") + gate_proj_weight = sd.get("model.layers.1.mlp.gate_proj.weight") + + # Fall back to layer 0 if layer 1 doesn't exist (single-layer model edge case) + if q_proj_weight is None: + q_proj_weight = sd.get("model.layers.0.self_attn.q_proj.weight") + k_proj_weight = sd.get("model.layers.0.self_attn.k_proj.weight") + gate_proj_weight = sd.get("model.layers.0.mlp.gate_proj.weight") + + if q_proj_weight is None or k_proj_weight is None or gate_proj_weight is None: + raise ValueError("Could not find attention/mlp weights in state dict to determine configuration") + + # Handle GGMLTensor shape access + q_shape = q_proj_weight.shape if hasattr(q_proj_weight, "shape") else q_proj_weight.tensor_shape + k_shape = k_proj_weight.shape if hasattr(k_proj_weight, "shape") else k_proj_weight.tensor_shape + gate_shape = gate_proj_weight.shape if hasattr(gate_proj_weight, "shape") else gate_proj_weight.tensor_shape + + # Calculate dimensions from actual weights + # IMPORTANT: Use hidden_size from k_proj input dimension (not q_proj or embed_tokens). + # Some models (like FLUX 2 Klein) have unusual architectures where: + # - embed_tokens has a larger dimension (e.g., 2560) + # - q_proj may have a larger input dimension for query expansion + # - k_proj/v_proj have the actual transformer hidden_size (e.g., 1280) + # Using k_proj ensures we get the correct internal hidden_size. + head_dim = 128 # Standard head dimension for Qwen3 models + hidden_size = k_shape[1] # Use k_proj input dim as the hidden_size + num_attention_heads = q_shape[0] // head_dim + num_kv_heads = k_shape[0] // head_dim + intermediate_size = gate_shape[0] + + logger.info( + f"Qwen3 GGUF Encoder config detected: layers={layer_count}, hidden={hidden_size}, " + f"heads={num_attention_heads}, kv_heads={num_kv_heads}, intermediate={intermediate_size}, " + f"head_dim={head_dim}" + ) + + # Create Qwen3 config + qwen_config = Qwen3Config( + vocab_size=vocab_size, + hidden_size=hidden_size, + intermediate_size=intermediate_size, + num_hidden_layers=layer_count, + num_attention_heads=num_attention_heads, + num_key_value_heads=num_kv_heads, + head_dim=head_dim, + max_position_embeddings=40960, + rms_norm_eps=1e-6, + tie_word_embeddings=True, + rope_theta=1000000.0, + use_sliding_window=False, + attention_bias=False, + attention_dropout=0.0, + torch_dtype=compute_dtype, + ) + + # Use Qwen3ForCausalLM with empty weights, then load GGUF tensors + with accelerate.init_empty_weights(): + model = Qwen3ForCausalLM(qwen_config) + + # Load the GGUF weights with assign=True + # GGMLTensor wrappers will be dequantized on-the-fly during inference + model.load_state_dict(sd, strict=False, assign=True) + + # Dequantize embed_tokens weight - embedding lookups require indexed access + # which quantized GGMLTensors can't efficiently provide (no __torch_dispatch__ for embedding) + from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor + + embed_tokens_weight = model.model.embed_tokens.weight + if isinstance(embed_tokens_weight, GGMLTensor): + dequantized = embed_tokens_weight.get_dequantized_tensor() + model.model.embed_tokens.weight = torch.nn.Parameter(dequantized, requires_grad=False) + logger.info("Dequantized embed_tokens weight for embedding lookups") + + # Handle tied weights - llama.cpp GGUF doesn't include lm_head.weight when embeddings are tied + # So we need to manually tie them after loading + if qwen_config.tie_word_embeddings: + # Check if lm_head.weight is still a meta tensor (wasn't in GGUF state dict) + if model.lm_head.weight.is_meta: + # Directly assign embed_tokens weight to lm_head (now dequantized) + model.lm_head.weight = model.model.embed_tokens.weight + logger.info("Tied lm_head.weight to embed_tokens.weight (GGUF tied embeddings)") + else: + # If lm_head.weight was loaded, use standard tie_weights + model.tie_weights() + + # Re-initialize any remaining meta tensor buffers (like rotary embeddings inv_freq) + for name, buffer in list(model.named_buffers()): + if buffer.is_meta: + parts = name.rsplit(".", 1) + if len(parts) == 2: + parent = model.get_submodule(parts[0]) + buffer_name = parts[1] + else: + parent = model + buffer_name = name + + if buffer_name == "inv_freq": + # Compute inv_freq from config - keep on CPU, cache system will move to GPU as needed + base = qwen_config.rope_theta + inv_freq = 1.0 / (base ** (torch.arange(0, head_dim, 2, dtype=torch.float32) / head_dim)) + parent.register_buffer(buffer_name, inv_freq.to(dtype=compute_dtype), persistent=False) + else: + logger.warning(f"Re-initializing unknown meta buffer: {name}") + + # Final check: ensure no meta tensors remain in parameters + meta_params = [(name, p) for name, p in model.named_parameters() if p.is_meta] + if meta_params: + meta_names = [name for name, _ in meta_params] + raise RuntimeError( + f"Failed to load all parameters from GGUF. The following remain as meta tensors: {meta_names}. " + "This may indicate missing keys in the GGUF file or a key mapping issue." + ) + + return model + + def _convert_llamacpp_to_pytorch(self, sd: dict[str, Any]) -> dict[str, Any]: + """Convert llama.cpp GGUF keys to PyTorch/HuggingFace format for Qwen models. + + llama.cpp format: + - blk.X.attn_q.weight -> model.layers.X.self_attn.q_proj.weight + - blk.X.attn_k.weight -> model.layers.X.self_attn.k_proj.weight + - blk.X.attn_v.weight -> model.layers.X.self_attn.v_proj.weight + - blk.X.attn_output.weight -> model.layers.X.self_attn.o_proj.weight + - blk.X.attn_q_norm.weight -> model.layers.X.self_attn.q_norm.weight (Qwen3 QK norm) + - blk.X.attn_k_norm.weight -> model.layers.X.self_attn.k_norm.weight (Qwen3 QK norm) + - blk.X.ffn_gate.weight -> model.layers.X.mlp.gate_proj.weight + - blk.X.ffn_up.weight -> model.layers.X.mlp.up_proj.weight + - blk.X.ffn_down.weight -> model.layers.X.mlp.down_proj.weight + - blk.X.attn_norm.weight -> model.layers.X.input_layernorm.weight + - blk.X.ffn_norm.weight -> model.layers.X.post_attention_layernorm.weight + - token_embd.weight -> model.embed_tokens.weight + - output_norm.weight -> model.norm.weight + - output.weight -> lm_head.weight (if not tied) + """ + import re + + key_map = { + "attn_q": "self_attn.q_proj", + "attn_k": "self_attn.k_proj", + "attn_v": "self_attn.v_proj", + "attn_output": "self_attn.o_proj", + "attn_q_norm": "self_attn.q_norm", # Qwen3 QK normalization + "attn_k_norm": "self_attn.k_norm", # Qwen3 QK normalization + "ffn_gate": "mlp.gate_proj", + "ffn_up": "mlp.up_proj", + "ffn_down": "mlp.down_proj", + "attn_norm": "input_layernorm", + "ffn_norm": "post_attention_layernorm", + } + + new_sd: dict[str, Any] = {} + blk_pattern = re.compile(r"^blk\.(\d+)\.(.+)$") + + for key, value in sd.items(): + if not isinstance(key, str): + new_sd[key] = value + continue + + # Handle block layers + match = blk_pattern.match(key) + if match: + layer_idx = match.group(1) + rest = match.group(2) + + # Split rest into component and suffix (e.g., "attn_q.weight" -> "attn_q", "weight") + parts = rest.split(".", 1) + component = parts[0] + suffix = parts[1] if len(parts) > 1 else "" + + if component in key_map: + new_component = key_map[component] + new_key = f"model.layers.{layer_idx}.{new_component}" + if suffix: + new_key += f".{suffix}" + new_sd[new_key] = value + else: + # Unknown component, keep as-is with model.layers prefix + new_sd[f"model.layers.{layer_idx}.{rest}"] = value + continue + + # Handle non-block keys + if key == "token_embd.weight": + new_sd["model.embed_tokens.weight"] = value + elif key == "output_norm.weight": + new_sd["model.norm.weight"] = value + elif key == "output.weight": + new_sd["lm_head.weight"] = value + else: + # Keep other keys as-is + new_sd[key] = value + + return new_sd diff --git a/invokeai/backend/model_manager/load/model_util.py b/invokeai/backend/model_manager/load/model_util.py new file mode 100644 index 00000000000..c3477fa6603 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_util.py @@ -0,0 +1,177 @@ +# Copyright (c) 2024 The InvokeAI Development Team +"""Various utility functions needed by the loader and caching system.""" + +import json +import logging +from pathlib import Path +from typing import Optional + +import onnxruntime as ort +import torch +from diffusers.pipelines.pipeline_utils import DiffusionPipeline +from diffusers.schedulers.scheduling_utils import SchedulerMixin +from transformers import CLIPTokenizer, PreTrainedTokenizerBase, T5Tokenizer, T5TokenizerFast + +from invokeai.backend.image_util.depth_anything.depth_anything_pipeline import DepthAnythingPipeline +from invokeai.backend.image_util.grounding_dino.grounding_dino_pipeline import GroundingDinoPipeline +from invokeai.backend.image_util.segment_anything.segment_anything_pipeline import SegmentAnythingPipeline +from invokeai.backend.ip_adapter.ip_adapter import IPAdapter +from invokeai.backend.model_manager.taxonomy import AnyModel +from invokeai.backend.onnx.onnx_runtime import IAIOnnxRuntimeModel +from invokeai.backend.patches.model_patch_raw import ModelPatchRaw +from invokeai.backend.spandrel_image_to_image_model import SpandrelImageToImageModel +from invokeai.backend.textual_inversion import TextualInversionModelRaw +from invokeai.backend.util.calc_tensor_size import calc_tensor_size + + +def calc_model_size_by_data(logger: logging.Logger, model: AnyModel) -> int: + """Get size of a model in memory in bytes.""" + # TODO(ryand): We should create a CacheableModel interface for all models, and move the size calculations down to + # the models themselves. + if isinstance(model, DiffusionPipeline): + return _calc_pipeline_by_data(model) + elif isinstance(model, torch.nn.Module): + return calc_module_size(model) + elif isinstance(model, IAIOnnxRuntimeModel): + return _calc_onnx_model_by_data(model) + elif isinstance(model, SchedulerMixin): + return 0 + elif isinstance(model, CLIPTokenizer): + # TODO(ryand): Accurately calculate the tokenizer's size. It's small enough that it shouldn't matter for now. + return 0 + elif isinstance( + model, + ( + TextualInversionModelRaw, + IPAdapter, + ModelPatchRaw, + SpandrelImageToImageModel, + GroundingDinoPipeline, + SegmentAnythingPipeline, + DepthAnythingPipeline, + ), + ): + return model.calc_size() + elif isinstance(model, ort.InferenceSession): + if model._model_bytes is not None: + # If the model is already loaded, return the size of the model bytes + return len(model._model_bytes) + elif model._model_path is not None: + # If the model is not loaded, return the size of the model path + return calc_model_size_by_fs(Path(model._model_path)) + else: + # If neither is available, return 0 + return 0 + elif isinstance( + model, + ( + T5TokenizerFast, + T5Tokenizer, + ), + ): + # HACK(ryand): len(model) just returns the vocabulary size, so this is blatantly wrong. It should be small + # relative to the text encoder that it's used with, so shouldn't matter too much, but we should fix this at some + # point. + return len(model) + elif isinstance(model, PreTrainedTokenizerBase): + # Catch-all for other tokenizer types (e.g., Qwen2Tokenizer, Qwen3Tokenizer). + # Tokenizers are small relative to models, so returning 0 is acceptable. + return 0 + else: + # TODO(ryand): Promote this from a log to an exception once we are confident that we are handling all of the + # supported model types. + logger.warning( + f"Failed to calculate model size for unexpected model type: {type(model)}. The model will be treated as " + "having size 0." + ) + return 0 + + +def _calc_pipeline_by_data(pipeline: DiffusionPipeline) -> int: + res = 0 + assert hasattr(pipeline, "components") + for submodel_key in pipeline.components.keys(): + submodel = getattr(pipeline, submodel_key) + if submodel is not None and isinstance(submodel, torch.nn.Module): + res += calc_module_size(submodel) + return res + + +def calc_module_size(model: torch.nn.Module) -> int: + """Calculate the size (in bytes) of a torch.nn.Module.""" + mem_params = sum([calc_tensor_size(param) for param in model.parameters()]) + mem_bufs = sum([calc_tensor_size(buf) for buf in model.buffers()]) + return mem_params + mem_bufs + + +def _calc_onnx_model_by_data(model: IAIOnnxRuntimeModel) -> int: + tensor_size = model.tensors.size() * 2 # The session doubles this + mem = tensor_size # in bytes + return mem + + +def calc_model_size_by_fs(model_path: Path, subfolder: Optional[str] = None, variant: Optional[str] = None) -> int: + """Estimate the size of a model on disk in bytes.""" + if model_path.is_file(): + return model_path.stat().st_size + + if subfolder is not None: + model_path = model_path / subfolder + + # this can happen when, for example, the safety checker is not downloaded. + if not model_path.exists(): + return 0 + + all_files = [f for f in model_path.iterdir() if (model_path / f).is_file()] + + fp16_files = {f for f in all_files if ".fp16." in f.name or ".fp16-" in f.name} + bit8_files = {f for f in all_files if ".8bit." in f.name or ".8bit-" in f.name} + other_files = set(all_files) - fp16_files - bit8_files + + if not variant: # ModelRepoVariant.DEFAULT evaluates to empty string for compatability with HF + files = other_files + elif variant == "fp16": + files = fp16_files + elif variant == "8bit": + files = bit8_files + else: + raise NotImplementedError(f"Unknown variant: {variant}") + + # try read from index if exists + index_postfix = ".index.json" + if variant is not None: + index_postfix = f".index.{variant}.json" + + for file in files: + if not file.name.endswith(index_postfix): + continue + try: + with open(model_path / file, "r") as f: + index_data = json.loads(f.read()) + return int(index_data["metadata"]["total_size"]) + except Exception: + pass + + # calculate files size if there is no index file + formats = [ + (".safetensors",), # safetensors + (".bin",), # torch + (".onnx", ".pb"), # onnx + (".msgpack",), # flax + (".ckpt",), # tf + (".h5",), # tf2 + (".gguf",), # gguf quantized + ] + + for file_format in formats: + model_files = [f for f in files if f.suffix in file_format] + if len(model_files) == 0: + continue + + model_size = 0 + for model_file in model_files: + file_stats = (model_path / model_file).stat() + model_size += file_stats.st_size + return model_size + + return 0 # scheduler/feature_extractor/tokenizer - models without loading to gpu diff --git a/invokeai/backend/model_manager/load/optimizations.py b/invokeai/backend/model_manager/load/optimizations.py new file mode 100644 index 00000000000..030fcfa639a --- /dev/null +++ b/invokeai/backend/model_manager/load/optimizations.py @@ -0,0 +1,31 @@ +from contextlib import contextmanager +from typing import Any, Generator + +import torch + + +def _no_op(*args: Any, **kwargs: Any) -> None: + pass + + +@contextmanager +def skip_torch_weight_init() -> Generator[None, None, None]: + """Monkey patch several of the common torch layers (torch.nn.Linear, torch.nn.Conv1d, etc.) to skip weight initialization. + + By default, `torch.nn.Linear` and `torch.nn.ConvNd` layers initialize their weights (according to a particular + distribution) when __init__ is called. This weight initialization step can take a significant amount of time, and is + completely unnecessary if the intent is to load checkpoint weights from disk for the layer. This context manager + monkey-patches common torch layers to skip the weight initialization step. + """ + torch_modules = [torch.nn.Linear, torch.nn.modules.conv._ConvNd, torch.nn.Embedding] + saved_functions = [hasattr(m, "reset_parameters") and m.reset_parameters for m in torch_modules] + + try: + for torch_module in torch_modules: + assert hasattr(torch_module, "reset_parameters") + torch_module.reset_parameters = _no_op + yield None + finally: + for torch_module, saved_function in zip(torch_modules, saved_functions, strict=True): + assert hasattr(torch_module, "reset_parameters") + torch_module.reset_parameters = saved_function diff --git a/invokeai/backend/model_manager/metadata/__init__.py b/invokeai/backend/model_manager/metadata/__init__.py new file mode 100644 index 00000000000..76da268153a --- /dev/null +++ b/invokeai/backend/model_manager/metadata/__init__.py @@ -0,0 +1,40 @@ +""" +Initialization file for invokeai.backend.model_manager.metadata + +Usage: + +from invokeai.backend.model_manager.metadata import( + AnyModelRepoMetadata, + CommercialUsage, + LicenseRestrictions, + HuggingFaceMetadata, +) + +from invokeai.backend.model_manager.metadata.fetch import HuggingFaceMetadataFetch + +data = HuggingFaceMetadataFetch().from_id("") +assert isinstance(data, HuggingFaceMetadata) +""" + +from invokeai.backend.model_manager.metadata.fetch import HuggingFaceMetadataFetch, ModelMetadataFetchBase +from invokeai.backend.model_manager.metadata.metadata_base import ( + AnyModelRepoMetadata, + AnyModelRepoMetadataValidator, + BaseMetadata, + HuggingFaceMetadata, + ModelMetadataWithFiles, + RemoteModelFile, + UnknownMetadataException, +) + +__all__ = [ + "AnyModelRepoMetadata", + "AnyModelRepoMetadataValidator", + "HuggingFaceMetadata", + "HuggingFaceMetadataFetch", + "ModelMetadataFetchBase", + "BaseMetadata", + "ModelMetadataWithFiles", + "RemoteModelFile", + "UnknownMetadataException", +] diff --git a/invokeai/backend/model_manager/metadata/fetch/__init__.py b/invokeai/backend/model_manager/metadata/fetch/__init__.py new file mode 100644 index 00000000000..62b3dc4d540 --- /dev/null +++ b/invokeai/backend/model_manager/metadata/fetch/__init__.py @@ -0,0 +1,16 @@ +""" +Initialization file for invokeai.backend.model_manager.metadata.fetch + +Usage: +from invokeai.backend.model_manager.metadata.fetch import ( + HuggingFaceMetadataFetch, +) + +data = HuggingFaceMetadataFetch().from_id("") +assert isinstance(data, HuggingFaceMetadata) +""" + +from invokeai.backend.model_manager.metadata.fetch.fetch_base import ModelMetadataFetchBase +from invokeai.backend.model_manager.metadata.fetch.huggingface import HuggingFaceMetadataFetch + +__all__ = ["ModelMetadataFetchBase", "HuggingFaceMetadataFetch"] diff --git a/invokeai/backend/model_manager/metadata/fetch/fetch_base.py b/invokeai/backend/model_manager/metadata/fetch/fetch_base.py new file mode 100644 index 00000000000..1dee3b1a75a --- /dev/null +++ b/invokeai/backend/model_manager/metadata/fetch/fetch_base.py @@ -0,0 +1,68 @@ +# Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Development Team + +""" +This module is the base class for subclasses that fetch metadata from model repositories + +Usage: + +from invokeai.backend.model_manager.metadata.fetch import HuggingFaceMetadataFetch + +data = HuggingFaceMetadataFetch().from_id("") +assert isinstance(data, HuggingFaceMetadata) +""" + +from abc import ABC, abstractmethod +from typing import Optional + +from pydantic.networks import AnyHttpUrl +from requests.sessions import Session + +from invokeai.backend.model_manager.metadata.metadata_base import ( + AnyModelRepoMetadata, + AnyModelRepoMetadataValidator, + BaseMetadata, +) +from invokeai.backend.model_manager.taxonomy import ModelRepoVariant + + +class ModelMetadataFetchBase(ABC): + """Fetch metadata from remote generative model repositories.""" + + @abstractmethod + def __init__(self, session: Optional[Session] = None): + """ + Initialize the fetcher with an optional requests.sessions.Session object. + + By providing a configurable Session object, we can support unit tests on + this module without an internet connection. + """ + pass + + @abstractmethod + def from_url(self, url: AnyHttpUrl) -> AnyModelRepoMetadata: + """ + Given a URL to a model repository, return a ModelMetadata object. + + This method will raise a `UnknownMetadataException` + in the event that the requested model metadata is not found at the provided location. + """ + pass + + @abstractmethod + def from_id(self, id: str, variant: Optional[ModelRepoVariant] = None) -> AnyModelRepoMetadata: + """ + Given an ID for a model, return a ModelMetadata object. + + :param id: An ID. + :param variant: A model variant from the ModelRepoVariant enum. + + This method will raise a `UnknownMetadataException` + in the event that the requested model's metadata is not found at the provided id. + """ + pass + + @classmethod + def from_json(cls, json: str) -> AnyModelRepoMetadata: + """Given the JSON representation of the metadata, return the corresponding Pydantic object.""" + metadata: BaseMetadata = AnyModelRepoMetadataValidator.validate_json(json) # type: ignore + return metadata diff --git a/invokeai/backend/model_manager/metadata/fetch/huggingface.py b/invokeai/backend/model_manager/metadata/fetch/huggingface.py new file mode 100644 index 00000000000..30fe418fe14 --- /dev/null +++ b/invokeai/backend/model_manager/metadata/fetch/huggingface.py @@ -0,0 +1,153 @@ +# Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Development Team + +""" +This module fetches model metadata objects from the HuggingFace model repository, +using either a `repo_id` or the model page URL. + +Usage: + +from invokeai.backend.model_manager.metadata.fetch import HuggingFaceMetadataFetch + +fetcher = HuggingFaceMetadataFetch() +metadata = fetcher.from_url("https://huggingface.co/stabilityai/sdxl-turbo") +print(metadata.tags) +""" + +import json +import re +from pathlib import Path +from typing import Optional + +import requests +from huggingface_hub import hf_hub_url +from pydantic.networks import AnyHttpUrl +from requests.sessions import Session + +from invokeai.backend.model_manager.metadata.fetch.fetch_base import ModelMetadataFetchBase +from invokeai.backend.model_manager.metadata.metadata_base import ( + AnyModelRepoMetadata, + HuggingFaceMetadata, + RemoteModelFile, + UnknownMetadataException, +) +from invokeai.backend.model_manager.taxonomy import ModelRepoVariant + +HF_MODEL_RE = r"https?://huggingface.co/([\w\-.]+/[\w\-.]+)" + + +class HuggingFaceMetadataFetch(ModelMetadataFetchBase): + """Fetch model metadata from HuggingFace.""" + + def __init__(self, session: Optional[Session] = None): + """ + Initialize the fetcher with an optional requests.sessions.Session object. + + By providing a configurable Session object, we can support unit tests on + this module without an internet connection. + """ + self._requests = session or requests.Session() + + @classmethod + def from_json(cls, json: str) -> HuggingFaceMetadata: + """Given the JSON representation of the metadata, return the corresponding Pydantic object.""" + metadata = HuggingFaceMetadata.model_validate_json(json) + return metadata + + def _fetch_model_info(self, repo_id: str, variant: Optional[ModelRepoVariant] = None) -> dict: + """Fetch model info from HuggingFace API using self._requests session. + + This allows the session to be mocked in tests via requests_testadapter. + """ + url = f"https://huggingface.co/api/models/{repo_id}" + params: dict[str, str] = {"blobs": "True"} + if variant is not None: + params["revision"] = str(variant) + + response = self._requests.get(url, params=params) + if response.status_code == 404: + raise UnknownMetadataException(f"'{repo_id}' not found.") + response.raise_for_status() + return response.json() + + def from_id(self, id: str, variant: Optional[ModelRepoVariant] = None) -> AnyModelRepoMetadata: + """Return a HuggingFaceMetadata object given the model's repo_id.""" + # Little loop which tries fetching a revision corresponding to the selected variant. + # If not available, then set variant to None and get the default. + # If this too fails, raise exception. + + model_info = None + + # Handling for our special syntax - we only want the base HF `org/repo` here. + repo_id = id.split("::")[0] or id + while not model_info: + try: + model_info = self._fetch_model_info(repo_id, variant) + except UnknownMetadataException: + raise + except requests.HTTPError: + if variant is None: + raise + else: + variant = None + + files: list[RemoteModelFile] = [] + + _, name = repo_id.split("/") + + for s in model_info.get("siblings") or []: + rfilename = s.get("rfilename") + size = s.get("size") + assert rfilename is not None + assert size is not None + lfs = s.get("lfs") + files.append( + RemoteModelFile( + url=hf_hub_url(repo_id, rfilename, revision=variant or "main"), + path=Path(name, rfilename), + size=size, + sha256=lfs.get("sha256") if lfs else None, + ) + ) + + # diffusers models have a `model_index.json` or `config.json` file + is_diffusers = any(str(f.url).endswith(("model_index.json", "config.json")) for f in files) + + # These URLs will be exposed to the user - I think these are the only file types we fully support + ckpt_urls = ( + None + if is_diffusers + else [ + f.url + for f in files + if str(f.url).endswith( + ( + ".safetensors", + ".bin", + ".pth", + ".pt", + ".ckpt", + ) + ) + ] + ) + + return HuggingFaceMetadata( + id=model_info["id"], + name=name, + files=files, + api_response=json.dumps(model_info, default=str), + is_diffusers=is_diffusers, + ckpt_urls=ckpt_urls, + ) + + def from_url(self, url: AnyHttpUrl) -> AnyModelRepoMetadata: + """ + Return a HuggingFaceMetadata object given the model's web page URL. + + In the case of an invalid or missing URL, raises a ModelNotFound exception. + """ + if match := re.match(HF_MODEL_RE, str(url), re.IGNORECASE): + repo_id = match.group(1) + return self.from_id(repo_id) + else: + raise UnknownMetadataException(f"'{url}' does not look like a HuggingFace model page") diff --git a/invokeai/backend/model_manager/metadata/metadata_base.py b/invokeai/backend/model_manager/metadata/metadata_base.py new file mode 100644 index 00000000000..b048144e547 --- /dev/null +++ b/invokeai/backend/model_manager/metadata/metadata_base.py @@ -0,0 +1,136 @@ +# Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Development Team + +"""This module defines core text-to-image model metadata fields. + +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. + +Note that the "name" and "description" are also present in `config` +records. This is intentional. The config record fields are intended to +be editable by the user as a form of customization. The metadata +versions of these fields are intended to be kept in sync with the +remote repo. +""" + +from pathlib import Path +from typing import List, Literal, Optional, Union + +from huggingface_hub import hf_hub_url +from pydantic import BaseModel, Field, TypeAdapter +from pydantic.networks import AnyHttpUrl +from requests.sessions import Session +from typing_extensions import Annotated + +from invokeai.backend.model_manager.taxonomy import ModelRepoVariant +from invokeai.backend.model_manager.util.select_hf_files import filter_files + + +class UnknownMetadataException(Exception): + """Raised when no metadata is available for a model.""" + + +class RemoteModelFile(BaseModel): + """Information about a downloadable file that forms part of a model.""" + + url: AnyHttpUrl = Field(description="The url to download this model file") + path: Path = Field(description="The path to the file, relative to the model root") + size: Optional[int] = Field(description="The size of this file, in bytes", default=0) + sha256: Optional[str] = Field(description="SHA256 hash of this model (not always available)", default=None) + + def __hash__(self) -> int: + return hash(str(self)) + + +class ModelMetadataBase(BaseModel): + """Base class for model metadata information.""" + + name: str = Field(description="model's name") + + +class BaseMetadata(ModelMetadataBase): + """Adds typing data for discriminated union.""" + + type: Literal["basemetadata"] = "basemetadata" + + +class ModelMetadataWithFiles(ModelMetadataBase): + """Base class for metadata that contains a list of downloadable model file(s).""" + + files: List[RemoteModelFile] = Field(description="model files and their sizes", default_factory=list) + + def download_urls( + self, + variant: Optional[ModelRepoVariant] = None, + subfolder: Optional[Path] = None, + session: Optional[Session] = None, + ) -> List[RemoteModelFile]: + """ + Return a list of URLs needed to download the model. + + :param variant: Return files needed to reconstruct the indicated variant (e.g. ModelRepoVariant('fp16')) + :param subfolder: Return files in the designated subfolder only + :param session: A request.Session object for offline testing + + Note that the "variant" and "subfolder" concepts currently only apply to HuggingFace. + However Civitai does have fields for the precision and format of its models, and may + provide variant selection criteria in the future. + """ + return self.files + + +class HuggingFaceMetadata(ModelMetadataWithFiles): + """Extended metadata fields provided by HuggingFace.""" + + type: Literal["huggingface"] = "huggingface" + id: str = Field(description="The HF model id") + api_response: Optional[str] = Field(description="Response from the HF API as stringified JSON", default=None) + is_diffusers: bool = Field(description="Whether the metadata is for a Diffusers format model", default=False) + ckpt_urls: Optional[List[AnyHttpUrl]] = Field( + description="URLs for all checkpoint format models in the metadata", default=None + ) + + def download_urls( + self, + variant: Optional[ModelRepoVariant] = None, + subfolder: Optional[Path] = None, + subfolders: Optional[List[Path]] = None, + session: Optional[Session] = None, + ) -> List[RemoteModelFile]: + """ + Return list of downloadable files, filtering by variant and subfolder(s), if any. + + :param variant: Return model files needed to reconstruct the indicated variant + :param subfolder: Return model files from the designated subfolder only (deprecated, use subfolders) + :param subfolders: Return model files from the designated subfolders + :param session: A request.Session object used for internet-free testing + + Note that there is special variant-filtering behavior here: + When the fp16 variant is requested and not available, the + full-precision model is returned. + """ + session = session or Session() + + paths = filter_files([x.path for x in self.files], variant, subfolder, subfolders) # all files in the model + + # Determine prefix for model_index.json check - only applies for single subfolder + prefix = "" + if subfolder and not subfolders: + prefix = f"{subfolder}/" + + # the next step reads model_index.json to determine which subdirectories belong + # to the model (only for single subfolder case) + if Path(f"{prefix}model_index.json") in paths: + url = hf_hub_url(self.id, filename="model_index.json", subfolder=str(subfolder) if subfolder else None) + resp = session.get(url) + resp.raise_for_status() + submodels = resp.json() + paths = [Path(subfolder or "", x) for x in paths if Path(x).parent.as_posix() in submodels] + paths.insert(0, Path(f"{prefix}model_index.json")) + + return [x for x in self.files if x.path in paths] + + +AnyModelRepoMetadata = Annotated[Union[BaseMetadata, HuggingFaceMetadata], Field(discriminator="type")] +AnyModelRepoMetadataValidator = TypeAdapter(AnyModelRepoMetadata) diff --git a/invokeai/backend/model_manager/model_on_disk.py b/invokeai/backend/model_manager/model_on_disk.py new file mode 100644 index 00000000000..acc413b54c0 --- /dev/null +++ b/invokeai/backend/model_manager/model_on_disk.py @@ -0,0 +1,144 @@ +from pathlib import Path +from typing import Any, Optional, TypeAlias + +import safetensors.torch +import torch +from picklescan.scanner import scan_file_path +from safetensors import safe_open + +from invokeai.app.services.config.config_default import get_config +from invokeai.backend.model_hash.model_hash import HASHING_ALGORITHMS, ModelHash +from invokeai.backend.model_manager.taxonomy import ModelRepoVariant +from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader +from invokeai.backend.util.logging import InvokeAILogger +from invokeai.backend.util.silence_warnings import SilenceWarnings + +StateDict: TypeAlias = dict[str | int, Any] # When are the keys int? + +logger = InvokeAILogger.get_logger() + + +class ModelOnDisk: + """A utility class representing a model stored on disk.""" + + def __init__(self, path: Path, hash_algo: HASHING_ALGORITHMS = "blake3_single"): + self.path = path + if self.path.suffix in {".safetensors", ".bin", ".pt", ".ckpt"}: + self.name = path.stem + else: + self.name = path.name + self.hash_algo = hash_algo + # Having a cache helps users of ModelOnDisk (i.e. configs) to save state + # This prevents redundant computations during matching and parsing + self._state_dict_cache: dict[Path, Any] = {} + self._metadata_cache: dict[Path, Any] = {} + + def hash(self) -> str: + return ModelHash(algorithm=self.hash_algo).hash(self.path) + + def size(self) -> int: + if self.path.is_file(): + return self.path.stat().st_size + return sum(file.stat().st_size for file in self.path.rglob("*")) + + def weight_files(self) -> set[Path]: + if self.path.is_file(): + return {self.path} + extensions = {".safetensors", ".pt", ".pth", ".ckpt", ".bin", ".gguf"} + return {f for f in self.path.rglob("*") if f.suffix in extensions and f.is_file()} + + def metadata(self, path: Optional[Path] = None) -> dict[str, str]: + path = path or self.path + if path in self._metadata_cache: + return self._metadata_cache[path] + try: + with safe_open(self.path, framework="pt", device="cpu") as f: + metadata = f.metadata() + assert isinstance(metadata, dict) + except Exception: + metadata = {} + + self._metadata_cache[path] = metadata + return metadata + + def repo_variant(self) -> Optional[ModelRepoVariant]: + if self.path.is_file(): + return None + + weight_files = list(self.path.glob("**/*.safetensors")) + weight_files.extend(list(self.path.glob("**/*.bin"))) + for x in weight_files: + if ".fp16" in x.suffixes: + return ModelRepoVariant.FP16 + if "openvino_model" in x.name: + return ModelRepoVariant.OpenVINO + if "flax_model" in x.name: + return ModelRepoVariant.Flax + if x.suffix == ".onnx": + return ModelRepoVariant.ONNX + return ModelRepoVariant.Default + + def load_state_dict(self, path: Optional[Path] = None) -> StateDict: + if path in self._state_dict_cache: + return self._state_dict_cache[path] + + path = self.resolve_weight_file(path) + + if path in self._state_dict_cache: + return self._state_dict_cache[path] + + with SilenceWarnings(): + if path.suffix.endswith((".ckpt", ".pt", ".pth", ".bin")): + scan_result = scan_file_path(path) + if scan_result.infected_files != 0: + if get_config().unsafe_disable_picklescan: + logger.warning( + f"The model {path.stem} is potentially infected by malware, but picklescan is disabled. " + "Proceeding with caution." + ) + else: + raise RuntimeError( + f"The model {path.stem} is potentially infected by malware. Aborting import." + ) + if scan_result.scan_err: + if get_config().unsafe_disable_picklescan: + logger.warning( + f"Error scanning the model at {path.stem} for malware, but picklescan is disabled. " + "Proceeding with caution." + ) + else: + raise RuntimeError(f"Error scanning the model at {path.stem} for malware. Aborting import.") + checkpoint = torch.load(path, map_location="cpu") + assert isinstance(checkpoint, dict) + elif path.suffix.endswith(".gguf"): + checkpoint = gguf_sd_loader(path, compute_dtype=torch.float32) + elif path.suffix.endswith(".safetensors"): + checkpoint = safetensors.torch.load_file(path) + else: + raise ValueError(f"Unrecognized model extension: {path.suffix}") + + state_dict = checkpoint.get("state_dict", checkpoint) + + # Normalize PEFT named-adapter keys (e.g. `lora_A.default.weight` → `lora_A.weight`). + # Pattern is LoRA-specific, so this is a no-op for non-LoRA state dicts. + from invokeai.backend.patches.lora_conversions.peft_adapter_utils import normalize_peft_adapter_names + + state_dict = normalize_peft_adapter_names(state_dict) + + self._state_dict_cache[path] = state_dict + return state_dict + + def resolve_weight_file(self, path: Optional[Path] = None) -> Path: + if not path: + weight_files = list(self.weight_files()) + match weight_files: + case []: + raise ValueError("No weight files found for this model") + case [p]: + return p + case ps if len(ps) >= 2: + raise ValueError( + f"Multiple weight files found for this model: {ps}. " + f"Please specify the intended file using the 'path' argument" + ) + return path diff --git a/invokeai/backend/model_manager/omi/__init__.py b/invokeai/backend/model_manager/omi/__init__.py new file mode 100644 index 00000000000..f941a216620 --- /dev/null +++ b/invokeai/backend/model_manager/omi/__init__.py @@ -0,0 +1,7 @@ +from invokeai.backend.model_manager.omi.omi import convert_from_omi +from invokeai.backend.model_manager.omi.vendor.model_spec.architecture import ( + flux_dev_1_lora, + stable_diffusion_xl_1_lora, +) + +__all__ = ["flux_dev_1_lora", "stable_diffusion_xl_1_lora", "convert_from_omi"] diff --git a/invokeai/backend/model_manager/omi/omi.py b/invokeai/backend/model_manager/omi/omi.py new file mode 100644 index 00000000000..b59c50da3a0 --- /dev/null +++ b/invokeai/backend/model_manager/omi/omi.py @@ -0,0 +1,21 @@ +from invokeai.backend.model_manager.model_on_disk import StateDict +from invokeai.backend.model_manager.omi.vendor.convert.lora import ( + convert_flux_lora as omi_flux, +) +from invokeai.backend.model_manager.omi.vendor.convert.lora import ( + convert_lora_util as lora_util, +) +from invokeai.backend.model_manager.omi.vendor.convert.lora import ( + convert_sdxl_lora as omi_sdxl, +) +from invokeai.backend.model_manager.taxonomy import BaseModelType + + +def convert_from_omi(weights_sd: StateDict, base: BaseModelType): + keyset = { + BaseModelType.Flux: omi_flux.convert_flux_lora_key_sets(), + BaseModelType.StableDiffusionXL: omi_sdxl.convert_sdxl_lora_key_sets(), + }[base] + source = "omi" + target = "legacy_diffusers" + return lora_util.__convert(weights_sd, keyset, source, target) # type: ignore diff --git a/invokeai/backend/model_manager/omi/vendor/__init__.py b/invokeai/backend/model_manager/omi/vendor/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/model_manager/omi/vendor/convert/__init__.py b/invokeai/backend/model_manager/omi/vendor/convert/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/model_manager/omi/vendor/convert/lora/__init__.py b/invokeai/backend/model_manager/omi/vendor/convert/lora/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/model_manager/omi/vendor/convert/lora/convert_clip.py b/invokeai/backend/model_manager/omi/vendor/convert/lora/convert_clip.py new file mode 100644 index 00000000000..93a94d74f46 --- /dev/null +++ b/invokeai/backend/model_manager/omi/vendor/convert/lora/convert_clip.py @@ -0,0 +1,20 @@ +from invokeai.backend.model_manager.omi.vendor.convert.lora.convert_lora_util import ( + LoraConversionKeySet, + map_prefix_range, +) + + +def map_clip(key_prefix: LoraConversionKeySet) -> list[LoraConversionKeySet]: + keys = [] + + keys += [LoraConversionKeySet("text_projection", "text_projection", parent=key_prefix)] + + for k in map_prefix_range("text_model.encoder.layers", "text_model.encoder.layers", parent=key_prefix): + keys += [LoraConversionKeySet("mlp.fc1", "mlp.fc1", parent=k)] + keys += [LoraConversionKeySet("mlp.fc2", "mlp.fc2", parent=k)] + keys += [LoraConversionKeySet("self_attn.k_proj", "self_attn.k_proj", parent=k)] + keys += [LoraConversionKeySet("self_attn.out_proj", "self_attn.out_proj", parent=k)] + keys += [LoraConversionKeySet("self_attn.q_proj", "self_attn.q_proj", parent=k)] + keys += [LoraConversionKeySet("self_attn.v_proj", "self_attn.v_proj", parent=k)] + + return keys diff --git a/invokeai/backend/model_manager/omi/vendor/convert/lora/convert_flux_lora.py b/invokeai/backend/model_manager/omi/vendor/convert/lora/convert_flux_lora.py new file mode 100644 index 00000000000..df6b775ff1c --- /dev/null +++ b/invokeai/backend/model_manager/omi/vendor/convert/lora/convert_flux_lora.py @@ -0,0 +1,84 @@ +from invokeai.backend.model_manager.omi.vendor.convert.lora.convert_clip import map_clip +from invokeai.backend.model_manager.omi.vendor.convert.lora.convert_lora_util import ( + LoraConversionKeySet, + map_prefix_range, +) +from invokeai.backend.model_manager.omi.vendor.convert.lora.convert_t5 import map_t5 + + +def __map_double_transformer_block(key_prefix: LoraConversionKeySet) -> list[LoraConversionKeySet]: + keys = [] + + keys += [LoraConversionKeySet("img_attn.qkv.0", "attn.to_q", parent=key_prefix)] + keys += [LoraConversionKeySet("img_attn.qkv.1", "attn.to_k", parent=key_prefix)] + keys += [LoraConversionKeySet("img_attn.qkv.2", "attn.to_v", parent=key_prefix)] + + keys += [LoraConversionKeySet("txt_attn.qkv.0", "attn.add_q_proj", parent=key_prefix)] + keys += [LoraConversionKeySet("txt_attn.qkv.1", "attn.add_k_proj", parent=key_prefix)] + keys += [LoraConversionKeySet("txt_attn.qkv.2", "attn.add_v_proj", parent=key_prefix)] + + keys += [LoraConversionKeySet("img_attn.proj", "attn.to_out.0", parent=key_prefix)] + keys += [LoraConversionKeySet("img_mlp.0", "ff.net.0.proj", parent=key_prefix)] + keys += [LoraConversionKeySet("img_mlp.2", "ff.net.2", parent=key_prefix)] + keys += [LoraConversionKeySet("img_mod.lin", "norm1.linear", parent=key_prefix)] + + keys += [LoraConversionKeySet("txt_attn.proj", "attn.to_add_out", parent=key_prefix)] + keys += [LoraConversionKeySet("txt_mlp.0", "ff_context.net.0.proj", parent=key_prefix)] + keys += [LoraConversionKeySet("txt_mlp.2", "ff_context.net.2", parent=key_prefix)] + keys += [LoraConversionKeySet("txt_mod.lin", "norm1_context.linear", parent=key_prefix)] + + return keys + + +def __map_single_transformer_block(key_prefix: LoraConversionKeySet) -> list[LoraConversionKeySet]: + keys = [] + + keys += [LoraConversionKeySet("linear1.0", "attn.to_q", parent=key_prefix)] + keys += [LoraConversionKeySet("linear1.1", "attn.to_k", parent=key_prefix)] + keys += [LoraConversionKeySet("linear1.2", "attn.to_v", parent=key_prefix)] + keys += [LoraConversionKeySet("linear1.3", "proj_mlp", parent=key_prefix)] + + keys += [LoraConversionKeySet("linear2", "proj_out", parent=key_prefix)] + keys += [LoraConversionKeySet("modulation.lin", "norm.linear", parent=key_prefix)] + + return keys + + +def __map_transformer(key_prefix: LoraConversionKeySet) -> list[LoraConversionKeySet]: + keys = [] + + keys += [LoraConversionKeySet("txt_in", "context_embedder", parent=key_prefix)] + keys += [ + LoraConversionKeySet("final_layer.adaLN_modulation.1", "norm_out.linear", parent=key_prefix, swap_chunks=True) + ] + keys += [LoraConversionKeySet("final_layer.linear", "proj_out", parent=key_prefix)] + keys += [ + LoraConversionKeySet("guidance_in.in_layer", "time_text_embed.guidance_embedder.linear_1", parent=key_prefix) + ] + keys += [ + LoraConversionKeySet("guidance_in.out_layer", "time_text_embed.guidance_embedder.linear_2", parent=key_prefix) + ] + keys += [LoraConversionKeySet("vector_in.in_layer", "time_text_embed.text_embedder.linear_1", parent=key_prefix)] + keys += [LoraConversionKeySet("vector_in.out_layer", "time_text_embed.text_embedder.linear_2", parent=key_prefix)] + keys += [LoraConversionKeySet("time_in.in_layer", "time_text_embed.timestep_embedder.linear_1", parent=key_prefix)] + keys += [LoraConversionKeySet("time_in.out_layer", "time_text_embed.timestep_embedder.linear_2", parent=key_prefix)] + keys += [LoraConversionKeySet("img_in.proj", "x_embedder", parent=key_prefix)] + + for k in map_prefix_range("double_blocks", "transformer_blocks", parent=key_prefix): + keys += __map_double_transformer_block(k) + + for k in map_prefix_range("single_blocks", "single_transformer_blocks", parent=key_prefix): + keys += __map_single_transformer_block(k) + + return keys + + +def convert_flux_lora_key_sets() -> list[LoraConversionKeySet]: + keys = [] + + keys += [LoraConversionKeySet("bundle_emb", "bundle_emb")] + keys += __map_transformer(LoraConversionKeySet("transformer", "lora_transformer")) + keys += map_clip(LoraConversionKeySet("clip_l", "lora_te1")) + keys += map_t5(LoraConversionKeySet("t5", "lora_te2")) + + return keys diff --git a/invokeai/backend/model_manager/omi/vendor/convert/lora/convert_lora_util.py b/invokeai/backend/model_manager/omi/vendor/convert/lora/convert_lora_util.py new file mode 100644 index 00000000000..a551d9b7d6d --- /dev/null +++ b/invokeai/backend/model_manager/omi/vendor/convert/lora/convert_lora_util.py @@ -0,0 +1,217 @@ +import torch +from torch import Tensor +from typing_extensions import Self + + +class LoraConversionKeySet: + def __init__( + self, + omi_prefix: str, + diffusers_prefix: str, + legacy_diffusers_prefix: str | None = None, + parent: Self | None = None, + swap_chunks: bool = False, + filter_is_last: bool | None = None, + next_omi_prefix: str | None = None, + next_diffusers_prefix: str | None = None, + ): + if parent is not None: + self.omi_prefix = combine(parent.omi_prefix, omi_prefix) + self.diffusers_prefix = combine(parent.diffusers_prefix, diffusers_prefix) + else: + self.omi_prefix = omi_prefix + self.diffusers_prefix = diffusers_prefix + + if legacy_diffusers_prefix is None: + self.legacy_diffusers_prefix = self.diffusers_prefix.replace(".", "_") + elif parent is not None: + self.legacy_diffusers_prefix = combine(parent.legacy_diffusers_prefix, legacy_diffusers_prefix).replace( + ".", "_" + ) + else: + self.legacy_diffusers_prefix = legacy_diffusers_prefix + + self.parent = parent + self.swap_chunks = swap_chunks + self.filter_is_last = filter_is_last + self.prefix = parent + + if next_omi_prefix is None and parent is not None: + self.next_omi_prefix = parent.next_omi_prefix + self.next_diffusers_prefix = parent.next_diffusers_prefix + self.next_legacy_diffusers_prefix = parent.next_legacy_diffusers_prefix + elif next_omi_prefix is not None and parent is not None: + self.next_omi_prefix = combine(parent.omi_prefix, next_omi_prefix) + self.next_diffusers_prefix = combine(parent.diffusers_prefix, next_diffusers_prefix) + self.next_legacy_diffusers_prefix = combine(parent.legacy_diffusers_prefix, next_diffusers_prefix).replace( + ".", "_" + ) + elif next_omi_prefix is not None and parent is None: + self.next_omi_prefix = next_omi_prefix + self.next_diffusers_prefix = next_diffusers_prefix + self.next_legacy_diffusers_prefix = next_diffusers_prefix.replace(".", "_") + else: + self.next_omi_prefix = None + self.next_diffusers_prefix = None + self.next_legacy_diffusers_prefix = None + + def __get_omi(self, in_prefix: str, key: str) -> str: + return self.omi_prefix + key.removeprefix(in_prefix) + + def __get_diffusers(self, in_prefix: str, key: str) -> str: + return self.diffusers_prefix + key.removeprefix(in_prefix) + + def __get_legacy_diffusers(self, in_prefix: str, key: str) -> str: + key = self.legacy_diffusers_prefix + key.removeprefix(in_prefix) + + suffix = key[key.rfind(".") :] + if suffix not in [".alpha", ".dora_scale"]: # some keys only have a single . in the suffix + suffix = key[key.removesuffix(suffix).rfind(".") :] + key = key.removesuffix(suffix) + + return key.replace(".", "_") + suffix + + def get_key(self, in_prefix: str, key: str, target: str) -> str: + if target == "omi": + return self.__get_omi(in_prefix, key) + elif target == "diffusers": + return self.__get_diffusers(in_prefix, key) + elif target == "legacy_diffusers": + return self.__get_legacy_diffusers(in_prefix, key) + return key + + def __str__(self) -> str: + return f"omi: {self.omi_prefix}, diffusers: {self.diffusers_prefix}, legacy: {self.legacy_diffusers_prefix}" + + +def combine(left: str, right: str) -> str: + left = left.rstrip(".") + right = right.lstrip(".") + if left == "" or left is None: + return right + elif right == "" or right is None: + return left + else: + return left + "." + right + + +def map_prefix_range( + omi_prefix: str, + diffusers_prefix: str, + parent: LoraConversionKeySet, +) -> list[LoraConversionKeySet]: + # 100 should be a safe upper bound. increase if it's not enough in the future + return [ + LoraConversionKeySet( + omi_prefix=f"{omi_prefix}.{i}", + diffusers_prefix=f"{diffusers_prefix}.{i}", + parent=parent, + next_omi_prefix=f"{omi_prefix}.{i + 1}", + next_diffusers_prefix=f"{diffusers_prefix}.{i + 1}", + ) + for i in range(100) + ] + + +def __convert( + state_dict: dict[str, Tensor], + key_sets: list[LoraConversionKeySet], + source: str, + target: str, +) -> dict[str, Tensor]: + out_states = {} + + if source == target: + return dict(state_dict) + + # TODO: maybe replace with a non O(n^2) algorithm + for key, tensor in state_dict.items(): + for key_set in key_sets: + in_prefix = "" + + if source == "omi": + in_prefix = key_set.omi_prefix + elif source == "diffusers": + in_prefix = key_set.diffusers_prefix + elif source == "legacy_diffusers": + in_prefix = key_set.legacy_diffusers_prefix + + if not key.startswith(in_prefix): + continue + + if key_set.filter_is_last is not None: + next_prefix = None + if source == "omi": + next_prefix = key_set.next_omi_prefix + elif source == "diffusers": + next_prefix = key_set.next_diffusers_prefix + elif source == "legacy_diffusers": + next_prefix = key_set.next_legacy_diffusers_prefix + + is_last = not any(k.startswith(next_prefix) for k in state_dict) + if key_set.filter_is_last != is_last: + continue + + name = key_set.get_key(in_prefix, key, target) + + can_swap_chunks = target == "omi" or source == "omi" + if key_set.swap_chunks and name.endswith(".lora_up.weight") and can_swap_chunks: + chunk_0, chunk_1 = tensor.chunk(2, dim=0) + tensor = torch.cat([chunk_1, chunk_0], dim=0) + + out_states[name] = tensor + + break # only map the first matching key set + + return out_states + + +def __detect_source( + state_dict: dict[str, Tensor], + key_sets: list[LoraConversionKeySet], +) -> str: + omi_count = 0 + diffusers_count = 0 + legacy_diffusers_count = 0 + + for key in state_dict: + for key_set in key_sets: + if key.startswith(key_set.omi_prefix): + omi_count += 1 + if key.startswith(key_set.diffusers_prefix): + diffusers_count += 1 + if key.startswith(key_set.legacy_diffusers_prefix): + legacy_diffusers_count += 1 + + if omi_count > diffusers_count and omi_count > legacy_diffusers_count: + return "omi" + if diffusers_count > omi_count and diffusers_count > legacy_diffusers_count: + return "diffusers" + if legacy_diffusers_count > omi_count and legacy_diffusers_count > diffusers_count: + return "legacy_diffusers" + + return "" + + +def convert_to_omi( + state_dict: dict[str, Tensor], + key_sets: list[LoraConversionKeySet], +) -> dict[str, Tensor]: + source = __detect_source(state_dict, key_sets) + return __convert(state_dict, key_sets, source, "omi") + + +def convert_to_diffusers( + state_dict: dict[str, Tensor], + key_sets: list[LoraConversionKeySet], +) -> dict[str, Tensor]: + source = __detect_source(state_dict, key_sets) + return __convert(state_dict, key_sets, source, "diffusers") + + +def convert_to_legacy_diffusers( + state_dict: dict[str, Tensor], + key_sets: list[LoraConversionKeySet], +) -> dict[str, Tensor]: + source = __detect_source(state_dict, key_sets) + return __convert(state_dict, key_sets, source, "legacy_diffusers") diff --git a/invokeai/backend/model_manager/omi/vendor/convert/lora/convert_sdxl_lora.py b/invokeai/backend/model_manager/omi/vendor/convert/lora/convert_sdxl_lora.py new file mode 100644 index 00000000000..68c293a7049 --- /dev/null +++ b/invokeai/backend/model_manager/omi/vendor/convert/lora/convert_sdxl_lora.py @@ -0,0 +1,125 @@ +from invokeai.backend.model_manager.omi.vendor.convert.lora.convert_clip import map_clip +from invokeai.backend.model_manager.omi.vendor.convert.lora.convert_lora_util import ( + LoraConversionKeySet, + map_prefix_range, +) + + +def __map_unet_resnet_block(key_prefix: LoraConversionKeySet) -> list[LoraConversionKeySet]: + keys = [] + + keys += [LoraConversionKeySet("emb_layers.1", "time_emb_proj", parent=key_prefix)] + keys += [LoraConversionKeySet("in_layers.2", "conv1", parent=key_prefix)] + keys += [LoraConversionKeySet("out_layers.3", "conv2", parent=key_prefix)] + keys += [LoraConversionKeySet("skip_connection", "conv_shortcut", parent=key_prefix)] + + return keys + + +def __map_unet_attention_block(key_prefix: LoraConversionKeySet) -> list[LoraConversionKeySet]: + keys = [] + + keys += [LoraConversionKeySet("proj_in", "proj_in", parent=key_prefix)] + keys += [LoraConversionKeySet("proj_out", "proj_out", parent=key_prefix)] + for k in map_prefix_range("transformer_blocks", "transformer_blocks", parent=key_prefix): + keys += [LoraConversionKeySet("attn1.to_q", "attn1.to_q", parent=k)] + keys += [LoraConversionKeySet("attn1.to_k", "attn1.to_k", parent=k)] + keys += [LoraConversionKeySet("attn1.to_v", "attn1.to_v", parent=k)] + keys += [LoraConversionKeySet("attn1.to_out.0", "attn1.to_out.0", parent=k)] + keys += [LoraConversionKeySet("attn2.to_q", "attn2.to_q", parent=k)] + keys += [LoraConversionKeySet("attn2.to_k", "attn2.to_k", parent=k)] + keys += [LoraConversionKeySet("attn2.to_v", "attn2.to_v", parent=k)] + keys += [LoraConversionKeySet("attn2.to_out.0", "attn2.to_out.0", parent=k)] + keys += [LoraConversionKeySet("ff.net.0.proj", "ff.net.0.proj", parent=k)] + keys += [LoraConversionKeySet("ff.net.2", "ff.net.2", parent=k)] + + return keys + + +def __map_unet_down_blocks(key_prefix: LoraConversionKeySet) -> list[LoraConversionKeySet]: + keys = [] + + keys += __map_unet_resnet_block(LoraConversionKeySet("1.0", "0.resnets.0", parent=key_prefix)) + keys += __map_unet_resnet_block(LoraConversionKeySet("2.0", "0.resnets.1", parent=key_prefix)) + keys += [LoraConversionKeySet("3.0.op", "0.downsamplers.0.conv", parent=key_prefix)] + + keys += __map_unet_resnet_block(LoraConversionKeySet("4.0", "1.resnets.0", parent=key_prefix)) + keys += __map_unet_attention_block(LoraConversionKeySet("4.1", "1.attentions.0", parent=key_prefix)) + keys += __map_unet_resnet_block(LoraConversionKeySet("5.0", "1.resnets.1", parent=key_prefix)) + keys += __map_unet_attention_block(LoraConversionKeySet("5.1", "1.attentions.1", parent=key_prefix)) + keys += [LoraConversionKeySet("6.0.op", "1.downsamplers.0.conv", parent=key_prefix)] + + keys += __map_unet_resnet_block(LoraConversionKeySet("7.0", "2.resnets.0", parent=key_prefix)) + keys += __map_unet_attention_block(LoraConversionKeySet("7.1", "2.attentions.0", parent=key_prefix)) + keys += __map_unet_resnet_block(LoraConversionKeySet("8.0", "2.resnets.1", parent=key_prefix)) + keys += __map_unet_attention_block(LoraConversionKeySet("8.1", "2.attentions.1", parent=key_prefix)) + + return keys + + +def __map_unet_mid_block(key_prefix: LoraConversionKeySet) -> list[LoraConversionKeySet]: + keys = [] + + keys += __map_unet_resnet_block(LoraConversionKeySet("0", "resnets.0", parent=key_prefix)) + keys += __map_unet_attention_block(LoraConversionKeySet("1", "attentions.0", parent=key_prefix)) + keys += __map_unet_resnet_block(LoraConversionKeySet("2", "resnets.1", parent=key_prefix)) + + return keys + + +def __map_unet_up_block(key_prefix: LoraConversionKeySet) -> list[LoraConversionKeySet]: + keys = [] + + keys += __map_unet_resnet_block(LoraConversionKeySet("0.0", "0.resnets.0", parent=key_prefix)) + keys += __map_unet_attention_block(LoraConversionKeySet("0.1", "0.attentions.0", parent=key_prefix)) + keys += __map_unet_resnet_block(LoraConversionKeySet("1.0", "0.resnets.1", parent=key_prefix)) + keys += __map_unet_attention_block(LoraConversionKeySet("1.1", "0.attentions.1", parent=key_prefix)) + keys += __map_unet_resnet_block(LoraConversionKeySet("2.0", "0.resnets.2", parent=key_prefix)) + keys += __map_unet_attention_block(LoraConversionKeySet("2.1", "0.attentions.2", parent=key_prefix)) + keys += [LoraConversionKeySet("2.2.conv", "0.upsamplers.0.conv", parent=key_prefix)] + + keys += __map_unet_resnet_block(LoraConversionKeySet("3.0", "1.resnets.0", parent=key_prefix)) + keys += __map_unet_attention_block(LoraConversionKeySet("3.1", "1.attentions.0", parent=key_prefix)) + keys += __map_unet_resnet_block(LoraConversionKeySet("4.0", "1.resnets.1", parent=key_prefix)) + keys += __map_unet_attention_block(LoraConversionKeySet("4.1", "1.attentions.1", parent=key_prefix)) + keys += __map_unet_resnet_block(LoraConversionKeySet("5.0", "1.resnets.2", parent=key_prefix)) + keys += __map_unet_attention_block(LoraConversionKeySet("5.1", "1.attentions.2", parent=key_prefix)) + keys += [LoraConversionKeySet("5.2.conv", "1.upsamplers.0.conv", parent=key_prefix)] + + keys += __map_unet_resnet_block(LoraConversionKeySet("6.0", "2.resnets.0", parent=key_prefix)) + keys += __map_unet_resnet_block(LoraConversionKeySet("7.0", "2.resnets.1", parent=key_prefix)) + keys += __map_unet_resnet_block(LoraConversionKeySet("8.0", "2.resnets.2", parent=key_prefix)) + + return keys + + +def __map_unet(key_prefix: LoraConversionKeySet) -> list[LoraConversionKeySet]: + keys = [] + + keys += [LoraConversionKeySet("input_blocks.0.0", "conv_in", parent=key_prefix)] + + keys += [LoraConversionKeySet("time_embed.0", "time_embedding.linear_1", parent=key_prefix)] + keys += [LoraConversionKeySet("time_embed.2", "time_embedding.linear_2", parent=key_prefix)] + + keys += [LoraConversionKeySet("label_emb.0.0", "add_embedding.linear_1", parent=key_prefix)] + keys += [LoraConversionKeySet("label_emb.0.2", "add_embedding.linear_2", parent=key_prefix)] + + keys += __map_unet_down_blocks(LoraConversionKeySet("input_blocks", "down_blocks", parent=key_prefix)) + keys += __map_unet_mid_block(LoraConversionKeySet("middle_block", "mid_block", parent=key_prefix)) + keys += __map_unet_up_block(LoraConversionKeySet("output_blocks", "up_blocks", parent=key_prefix)) + + keys += [LoraConversionKeySet("out.0", "conv_norm_out", parent=key_prefix)] + keys += [LoraConversionKeySet("out.2", "conv_out", parent=key_prefix)] + + return keys + + +def convert_sdxl_lora_key_sets() -> list[LoraConversionKeySet]: + keys = [] + + keys += [LoraConversionKeySet("bundle_emb", "bundle_emb")] + keys += __map_unet(LoraConversionKeySet("unet", "lora_unet")) + keys += map_clip(LoraConversionKeySet("clip_l", "lora_te1")) + keys += map_clip(LoraConversionKeySet("clip_g", "lora_te2")) + + return keys diff --git a/invokeai/backend/model_manager/omi/vendor/convert/lora/convert_t5.py b/invokeai/backend/model_manager/omi/vendor/convert/lora/convert_t5.py new file mode 100644 index 00000000000..94724a3d9f6 --- /dev/null +++ b/invokeai/backend/model_manager/omi/vendor/convert/lora/convert_t5.py @@ -0,0 +1,19 @@ +from invokeai.backend.model_manager.omi.vendor.convert.lora.convert_lora_util import ( + LoraConversionKeySet, + map_prefix_range, +) + + +def map_t5(key_prefix: LoraConversionKeySet) -> list[LoraConversionKeySet]: + keys = [] + + for k in map_prefix_range("encoder.block", "encoder.block", parent=key_prefix): + keys += [LoraConversionKeySet("layer.0.SelfAttention.k", "layer.0.SelfAttention.k", parent=k)] + keys += [LoraConversionKeySet("layer.0.SelfAttention.o", "layer.0.SelfAttention.o", parent=k)] + keys += [LoraConversionKeySet("layer.0.SelfAttention.q", "layer.0.SelfAttention.q", parent=k)] + keys += [LoraConversionKeySet("layer.0.SelfAttention.v", "layer.0.SelfAttention.v", parent=k)] + keys += [LoraConversionKeySet("layer.1.DenseReluDense.wi_0", "layer.1.DenseReluDense.wi_0", parent=k)] + keys += [LoraConversionKeySet("layer.1.DenseReluDense.wi_1", "layer.1.DenseReluDense.wi_1", parent=k)] + keys += [LoraConversionKeySet("layer.1.DenseReluDense.wo", "layer.1.DenseReluDense.wo", parent=k)] + + return keys diff --git a/invokeai/backend/model_manager/omi/vendor/model_spec/__init__.py b/invokeai/backend/model_manager/omi/vendor/model_spec/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/model_manager/omi/vendor/model_spec/architecture.py b/invokeai/backend/model_manager/omi/vendor/model_spec/architecture.py new file mode 100644 index 00000000000..3e02d537758 --- /dev/null +++ b/invokeai/backend/model_manager/omi/vendor/model_spec/architecture.py @@ -0,0 +1,31 @@ +stable_diffusion_1_lora = "stable-diffusion-v1/lora" +stable_diffusion_1_inpainting_lora = "stable-diffusion-v1-inpainting/lora" + +stable_diffusion_2_512_lora = "stable-diffusion-v2-512/lora" +stable_diffusion_2_768_v_lora = "stable-diffusion-v2-768-v/lora" +stable_diffusion_2_depth_lora = "stable-diffusion-v2-depth/lora" +stable_diffusion_2_inpainting_lora = "stable-diffusion-v2-inpainting/lora" + +stable_diffusion_3_medium_lora = "stable-diffusion-v3-medium/lora" +stable_diffusion_35_medium_lora = "stable-diffusion-v3.5-medium/lora" +stable_diffusion_35_large_lora = "stable-diffusion-v3.5-large/lora" + +stable_diffusion_xl_1_lora = "stable-diffusion-xl-v1-base/lora" +stable_diffusion_xl_1_inpainting_lora = "stable-diffusion-xl-v1-base-inpainting/lora" + +wuerstchen_2_lora = "wuerstchen-v2-prior/lora" +stable_cascade_1_stage_a_lora = "stable-cascade-v1-stage-a/lora" +stable_cascade_1_stage_b_lora = "stable-cascade-v1-stage-b/lora" +stable_cascade_1_stage_c_lora = "stable-cascade-v1-stage-c/lora" + +pixart_alpha_lora = "pixart-alpha/lora" +pixart_sigma_lora = "pixart-sigma/lora" + +flux_dev_1_lora = "Flux.1-dev/lora" +flux_fill_dev_1_lora = "Flux.1-fill-dev/lora" + +sana_lora = "sana/lora" + +hunyuan_video_lora = "hunyuan-video/lora" + +hi_dream_i1_lora = "hidream-i1/lora" diff --git a/invokeai/backend/model_manager/search.py b/invokeai/backend/model_manager/search.py new file mode 100644 index 00000000000..a0569798804 --- /dev/null +++ b/invokeai/backend/model_manager/search.py @@ -0,0 +1,142 @@ +# Copyright 2023, Lincoln D. Stein and the InvokeAI Team +""" +Abstract base class and implementation for recursive directory search for models. + +Example usage: +``` + from invokeai.backend.model_manager import ModelSearch, ModelProbe + + def find_main_models(model: Path) -> bool: + info = ModelProbe.probe(model) + if info.model_type == 'main' and info.base_type == 'sd-1': + return True + else: + return False + + search = ModelSearch(on_model_found=report_it) + found = search.search('/tmp/models') + print(found) # list of matching model paths + print(search.stats) # search stats +``` +""" + +import os +from dataclasses import dataclass +from pathlib import Path +from typing import Callable, Optional + +from invokeai.backend.util.logging import InvokeAILogger + + +@dataclass +class SearchStats: + """Statistics about the search. + + Attributes: + items_scanned: number of items scanned + models_found: number of models found + models_filtered: number of models that passed the filter + """ + + items_scanned = 0 + models_found = 0 + models_filtered = 0 + + +class ModelSearch: + """Searches a directory tree for models, using a callback to filter the results. + + Usage: + search = ModelSearch() + search.on_model_found = lambda path : 'anime' in path.as_posix() + found = search.search(Path('/tmp/models1')) + """ + + def __init__( + self, + on_search_started: Optional[Callable[[Path], None]] = None, + on_model_found: Optional[Callable[[Path], bool]] = None, + on_search_completed: Optional[Callable[[set[Path]], None]] = None, + ) -> None: + """Create a new ModelSearch object. + + Args: + on_search_started: callback to be invoked when the search starts + on_model_found: callback to be invoked when a model is found. The callback should return True if the model + should be included in the results. + on_search_completed: callback to be invoked when the search is completed + """ + self.stats = SearchStats() + self.logger = InvokeAILogger.get_logger() + self.on_search_started = on_search_started + self.on_model_found = on_model_found + self.on_search_completed = on_search_completed + self.models_found: set[Path] = set() + + def search_started(self) -> None: + self.models_found = set() + if self.on_search_started: + self.on_search_started(self._directory) + + def model_found(self, model: Path) -> None: + self.stats.models_found += 1 + if self.on_model_found is None or self.on_model_found(model): + self.stats.models_filtered += 1 + self.models_found.add(model) + + def search_completed(self) -> None: + if self.on_search_completed is not None: + self.on_search_completed(self.models_found) + + def search(self, directory: Path) -> set[Path]: + self._directory = Path(directory) + self._directory = self._directory.resolve() + self.stats = SearchStats() # zero out + self.search_started() # This will initialize _models_found to empty + self._walk_directory(self._directory) + self.search_completed() + return self.models_found + + def _walk_directory(self, path: Path, max_depth: int = 20) -> None: + """Recursively walk the directory tree, looking for models.""" + absolute_path = Path(path) + if ( + len(absolute_path.parts) - len(self._directory.parts) > max_depth + or not absolute_path.exists() + or absolute_path.parent in self.models_found + ): + return + entries = os.scandir(absolute_path.as_posix()) + entries = [entry for entry in entries if not entry.name.startswith(".")] + dirs = [entry for entry in entries if entry.is_dir()] + file_names = [entry.name for entry in entries if entry.is_file()] + if any( + x in file_names + for x in [ + "config.json", + "model_index.json", + "learned_embeds.bin", + "pytorch_lora_weights.bin", + "image_encoder.txt", + ] + ): + try: + self.model_found(absolute_path) + return + except KeyboardInterrupt: + raise + except Exception as e: + self.logger.warning(str(e)) + return + + for n in file_names: + if n.endswith((".ckpt", ".bin", ".pth", ".safetensors", ".pt", ".gguf")): + try: + self.model_found(absolute_path / n) + except KeyboardInterrupt: + raise + except Exception as e: + self.logger.warning(str(e)) + + for d in dirs: + self._walk_directory(absolute_path / d) diff --git a/invokeai/backend/model_manager/single_file_config_files.py b/invokeai/backend/model_manager/single_file_config_files.py new file mode 100644 index 00000000000..fa4b9e934b8 --- /dev/null +++ b/invokeai/backend/model_manager/single_file_config_files.py @@ -0,0 +1,93 @@ +from dataclasses import dataclass + +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.taxonomy import ( + BaseModelType, + ModelType, + ModelVariantType, + SchedulerPredictionType, +) + + +@dataclass(frozen=True) +class LegacyConfigKey: + type: ModelType + base: BaseModelType + variant: ModelVariantType | None = None + pred: SchedulerPredictionType | None = None + + @classmethod + def from_model_config(cls, config: AnyModelConfig) -> "LegacyConfigKey": + variant = getattr(config, "variant", None) + pred = getattr(config, "prediction_type", None) + return cls(type=config.type, base=config.base, variant=variant, pred=pred) + + +LEGACY_CONFIG_MAP: dict[LegacyConfigKey, str] = { + LegacyConfigKey( + ModelType.Main, + BaseModelType.StableDiffusion1, + ModelVariantType.Normal, + SchedulerPredictionType.Epsilon, + ): "stable-diffusion/v1-inference.yaml", + LegacyConfigKey( + ModelType.Main, + BaseModelType.StableDiffusion1, + ModelVariantType.Normal, + SchedulerPredictionType.VPrediction, + ): "stable-diffusion/v1-inference-v.yaml", + LegacyConfigKey( + ModelType.Main, + BaseModelType.StableDiffusion1, + ModelVariantType.Inpaint, + ): "stable-diffusion/v1-inpainting-inference.yaml", + LegacyConfigKey( + ModelType.Main, + BaseModelType.StableDiffusion2, + ModelVariantType.Normal, + SchedulerPredictionType.Epsilon, + ): "stable-diffusion/v2-inference.yaml", + LegacyConfigKey( + ModelType.Main, + BaseModelType.StableDiffusion2, + ModelVariantType.Normal, + SchedulerPredictionType.VPrediction, + ): "stable-diffusion/v2-inference-v.yaml", + LegacyConfigKey( + ModelType.Main, + BaseModelType.StableDiffusion2, + ModelVariantType.Inpaint, + SchedulerPredictionType.Epsilon, + ): "stable-diffusion/v2-inpainting-inference.yaml", + LegacyConfigKey( + ModelType.Main, + BaseModelType.StableDiffusion2, + ModelVariantType.Inpaint, + SchedulerPredictionType.VPrediction, + ): "stable-diffusion/v2-inpainting-inference-v.yaml", + LegacyConfigKey( + ModelType.Main, + BaseModelType.StableDiffusion2, + ModelVariantType.Depth, + ): "stable-diffusion/v2-midas-inference.yaml", + LegacyConfigKey( + ModelType.Main, + BaseModelType.StableDiffusionXL, + ModelVariantType.Normal, + ): "stable-diffusion/sd_xl_base.yaml", + LegacyConfigKey( + ModelType.Main, + BaseModelType.StableDiffusionXL, + ModelVariantType.Inpaint, + ): "stable-diffusion/sd_xl_inpaint.yaml", + LegacyConfigKey( + ModelType.Main, + BaseModelType.StableDiffusionXLRefiner, + ModelVariantType.Normal, + ): "stable-diffusion/sd_xl_refiner.yaml", + LegacyConfigKey(ModelType.ControlNet, BaseModelType.StableDiffusion1): "controlnet/cldm_v15.yaml", + LegacyConfigKey(ModelType.ControlNet, BaseModelType.StableDiffusion2): "controlnet/cldm_v21.yaml", + LegacyConfigKey(ModelType.VAE, BaseModelType.StableDiffusion1): "stable-diffusion/v1-inference.yaml", + LegacyConfigKey(ModelType.VAE, BaseModelType.StableDiffusion2): "stable-diffusion/v2-inference.yaml", + LegacyConfigKey(ModelType.VAE, BaseModelType.StableDiffusionXL): "stable-diffusion/sd_xl_base.yaml", +} diff --git a/invokeai/backend/model_manager/starter_models.py b/invokeai/backend/model_manager/starter_models.py new file mode 100644 index 00000000000..9bc58e44269 --- /dev/null +++ b/invokeai/backend/model_manager/starter_models.py @@ -0,0 +1,1812 @@ +from typing import Optional + +from pydantic import BaseModel + +from invokeai.backend.model_manager.configs.external_api import ( + ExternalApiModelDefaultSettings, + ExternalImageSize, + ExternalModelCapabilities, + ExternalModelPanelSchema, + ExternalResolutionPreset, +) +from invokeai.backend.model_manager.taxonomy import ( + AnyVariant, + BaseModelType, + ModelFormat, + ModelType, + QwenImageVariantType, +) + + +class StarterModelWithoutDependencies(BaseModel): + description: str + source: str + name: str + base: BaseModelType + type: ModelType + format: Optional[ModelFormat] = None + variant: Optional[AnyVariant] = None + is_installed: bool = False + capabilities: ExternalModelCapabilities | None = None + default_settings: ExternalApiModelDefaultSettings | None = None + panel_schema: ExternalModelPanelSchema | None = None + # allows us to track what models a user has installed across name changes within starter models + # if you update a starter model name, please add the old one to this list for that starter model + previous_names: list[str] = [] + + +class StarterModel(StarterModelWithoutDependencies): + # Optional list of model source dependencies that need to be installed before this model can be used + dependencies: Optional[list[StarterModelWithoutDependencies]] = None + + +class StarterModelBundle(BaseModel): + name: str + models: list[StarterModel] + + +cyberrealistic_negative = StarterModel( + name="CyberRealistic Negative v3", + base=BaseModelType.StableDiffusion1, + source="https://huggingface.co/cyberdelia/CyberRealistic_Negative/resolve/main/CyberRealistic_Negative_v3.pt", + description="Negative embedding specifically for use with CyberRealistic.", + type=ModelType.TextualInversion, +) + +# region CLIP Image Encoders + +# This is CLIP-ViT-H-14-laion2B-s32B-b79K +ip_adapter_sd_image_encoder = StarterModel( + name="IP Adapter SD1.5 Image Encoder", + base=BaseModelType.Any, + source="InvokeAI/ip_adapter_sd_image_encoder", + description="IP Adapter SD Image Encoder", + type=ModelType.CLIPVision, +) + +# This is CLIP-ViT-bigG-14-laion2B-39B-b160k +ip_adapter_sdxl_image_encoder = StarterModel( + name="IP Adapter SDXL Image Encoder", + base=BaseModelType.Any, + source="InvokeAI/ip_adapter_sdxl_image_encoder", + description="IP Adapter SDXL Image Encoder", + type=ModelType.CLIPVision, +) +# Note: This model is installed from the same source as the CLIPEmbed model below. The model contains both the image +# encoder and the text encoder, but we need separate model entries so that they get loaded correctly. +clip_vit_l_image_encoder = StarterModel( + name="clip-vit-large-patch14", + base=BaseModelType.Any, + source="InvokeAI/clip-vit-large-patch14", + description="CLIP ViT-L Image Encoder", + type=ModelType.CLIPVision, +) +# endregion + +# region TextEncoders +t5_base_encoder = StarterModel( + name="t5_base_encoder", + base=BaseModelType.Any, + source="InvokeAI/t5-v1_1-xxl::bfloat16", + description="T5-XXL text encoder (used in FLUX pipelines). ~9.5GB", + type=ModelType.T5Encoder, +) + +t5_8b_quantized_encoder = StarterModel( + name="t5_bnb_int8_quantized_encoder", + base=BaseModelType.Any, + source="InvokeAI/t5-v1_1-xxl::bnb_llm_int8", + description="T5-XXL text encoder with bitsandbytes LLM.int8() quantization (used in FLUX pipelines). ~5GB", + type=ModelType.T5Encoder, + format=ModelFormat.BnbQuantizedLlmInt8b, +) + +clip_l_encoder = StarterModel( + name="clip-vit-large-patch14", + base=BaseModelType.Any, + source="InvokeAI/clip-vit-large-patch14-text-encoder::bfloat16", + description="CLIP-L text encoder (used in FLUX pipelines). ~250MB", + type=ModelType.CLIPEmbed, +) +# endregion + +# region VAE +sdxl_fp16_vae_fix = StarterModel( + name="sdxl-vae-fp16-fix", + base=BaseModelType.StableDiffusionXL, + source="madebyollin/sdxl-vae-fp16-fix", + description="SDXL VAE that works with FP16.", + type=ModelType.VAE, +) +flux_vae = StarterModel( + name="FLUX.1-schnell_ae", + base=BaseModelType.Flux, + source="black-forest-labs/FLUX.1-schnell::ae.safetensors", + description="FLUX VAE compatible with both schnell and dev variants.", + type=ModelType.VAE, +) +# endregion + + +# region: Main +flux_schnell_quantized = StarterModel( + name="FLUX.1 schnell (quantized)", + base=BaseModelType.Flux, + source="InvokeAI/flux_schnell::transformer/bnb_nf4/flux1-schnell-bnb_nf4.safetensors", + description="FLUX schnell transformer quantized to bitsandbytes NF4 format. Total size with dependencies: ~12GB", + type=ModelType.Main, + dependencies=[t5_8b_quantized_encoder, flux_vae, clip_l_encoder], +) +flux_dev_quantized = StarterModel( + name="FLUX.1 dev (quantized)", + base=BaseModelType.Flux, + source="InvokeAI/flux_dev::transformer/bnb_nf4/flux1-dev-bnb_nf4.safetensors", + description="FLUX dev transformer quantized to bitsandbytes NF4 format. Total size with dependencies: ~12GB", + type=ModelType.Main, + dependencies=[t5_8b_quantized_encoder, flux_vae, clip_l_encoder], +) +flux_schnell = StarterModel( + name="FLUX.1 schnell", + base=BaseModelType.Flux, + source="InvokeAI/flux_schnell::transformer/base/flux1-schnell.safetensors", + description="FLUX schnell transformer in bfloat16. Total size with dependencies: ~33GB", + type=ModelType.Main, + dependencies=[t5_base_encoder, flux_vae, clip_l_encoder], +) +flux_dev = StarterModel( + name="FLUX.1 dev", + base=BaseModelType.Flux, + source="InvokeAI/flux_dev::transformer/base/flux1-dev.safetensors", + description="FLUX dev transformer in bfloat16. Total size with dependencies: ~33GB", + type=ModelType.Main, + dependencies=[t5_base_encoder, flux_vae, clip_l_encoder], +) +flux_kontext = StarterModel( + name="FLUX.1 Kontext dev", + base=BaseModelType.Flux, + source="https://huggingface.co/black-forest-labs/FLUX.1-Kontext-dev/resolve/main/flux1-kontext-dev.safetensors", + description="FLUX.1 Kontext dev transformer in bfloat16. Total size with dependencies: ~33GB", + type=ModelType.Main, + dependencies=[t5_base_encoder, flux_vae, clip_l_encoder], +) +flux_kontext_quantized = StarterModel( + name="FLUX.1 Kontext dev (quantized)", + base=BaseModelType.Flux, + source="https://huggingface.co/unsloth/FLUX.1-Kontext-dev-GGUF/resolve/main/flux1-kontext-dev-Q4_K_M.gguf", + description="FLUX.1 Kontext dev quantized (q4_k_m). Total size with dependencies: ~12GB", + type=ModelType.Main, + dependencies=[t5_8b_quantized_encoder, flux_vae, clip_l_encoder], +) +flux_krea = StarterModel( + name="FLUX.1 Krea dev", + base=BaseModelType.Flux, + source="https://huggingface.co/InvokeAI/FLUX.1-Krea-dev/resolve/main/flux1-krea-dev.safetensors", + description="FLUX.1 Krea dev. Total size with dependencies: ~29GB", + type=ModelType.Main, + dependencies=[t5_8b_quantized_encoder, flux_vae, clip_l_encoder], +) +flux_krea_quantized = StarterModel( + name="FLUX.1 Krea dev (quantized)", + base=BaseModelType.Flux, + source="https://huggingface.co/InvokeAI/FLUX.1-Krea-dev-GGUF/resolve/main/flux1-krea-dev-Q4_K_M.gguf", + description="FLUX.1 Krea dev quantized (q4_k_m). Total size with dependencies: ~12GB", + type=ModelType.Main, + dependencies=[t5_8b_quantized_encoder, flux_vae, clip_l_encoder], +) +sd35_medium = StarterModel( + name="SD3.5 Medium", + base=BaseModelType.StableDiffusion3, + source="stabilityai/stable-diffusion-3.5-medium", + description="Medium SD3.5 Model: ~16GB", + type=ModelType.Main, + dependencies=[], +) +sd35_large = StarterModel( + name="SD3.5 Large", + base=BaseModelType.StableDiffusion3, + source="stabilityai/stable-diffusion-3.5-large", + description="Large SD3.5 Model: ~28GB", + type=ModelType.Main, + dependencies=[], +) +cyberrealistic_sd1 = StarterModel( + name="CyberRealistic v4.1", + base=BaseModelType.StableDiffusion1, + source="https://huggingface.co/cyberdelia/CyberRealistic/resolve/main/CyberRealistic_V4.1_FP16.safetensors", + description="Photorealistic model. See other variants in HF repo 'cyberdelia/CyberRealistic'.", + type=ModelType.Main, + dependencies=[cyberrealistic_negative], +) +rev_animated_sd1 = StarterModel( + name="ReV Animated", + base=BaseModelType.StableDiffusion1, + source="stablediffusionapi/rev-animated", + description="Fantasy and anime style images.", + type=ModelType.Main, +) +dreamshaper_8_sd1 = StarterModel( + name="Dreamshaper 8", + base=BaseModelType.StableDiffusion1, + source="Lykon/dreamshaper-8", + description="Popular versatile model.", + type=ModelType.Main, +) +dreamshaper_8_inpainting_sd1 = StarterModel( + name="Dreamshaper 8 (inpainting)", + base=BaseModelType.StableDiffusion1, + source="Lykon/dreamshaper-8-inpainting", + description="Inpainting version of Dreamshaper 8.", + type=ModelType.Main, +) +deliberate_sd1 = StarterModel( + name="Deliberate v5", + base=BaseModelType.StableDiffusion1, + source="https://huggingface.co/XpucT/Deliberate/resolve/main/Deliberate_v5.safetensors", + description="Popular versatile model", + type=ModelType.Main, +) +deliberate_inpainting_sd1 = StarterModel( + name="Deliberate v5 (inpainting)", + base=BaseModelType.StableDiffusion1, + source="https://huggingface.co/XpucT/Deliberate/resolve/main/Deliberate_v5-inpainting.safetensors", + description="Inpainting version of Deliberate v5.", + type=ModelType.Main, +) +juggernaut_sdxl = StarterModel( + name="Juggernaut XL v9", + base=BaseModelType.StableDiffusionXL, + source="RunDiffusion/Juggernaut-XL-v9", + description="Photograph-focused model.", + type=ModelType.Main, + dependencies=[sdxl_fp16_vae_fix], +) +dreamshaper_sdxl = StarterModel( + name="Dreamshaper XL v2 Turbo", + base=BaseModelType.StableDiffusionXL, + source="Lykon/dreamshaper-xl-v2-turbo", + description="For turbo, use CFG Scale 2, 4-8 steps, DPM++ SDE Karras. For non-turbo, use CFG Scale 6, 20-40 steps, DPM++ 2M SDE Karras.", + type=ModelType.Main, + dependencies=[sdxl_fp16_vae_fix], +) + +archvis_sdxl = StarterModel( + name="Architecture (RealVisXL5)", + base=BaseModelType.StableDiffusionXL, + source="SG161222/RealVisXL_V5.0", + description="A photorealistic model, with architecture among its many use cases", + type=ModelType.Main, + dependencies=[sdxl_fp16_vae_fix], +) + +sdxl_refiner = StarterModel( + name="SDXL Refiner", + base=BaseModelType.StableDiffusionXLRefiner, + source="stabilityai/stable-diffusion-xl-refiner-1.0", + description="The OG Stable Diffusion XL refiner model.", + type=ModelType.Main, + dependencies=[sdxl_fp16_vae_fix], +) +# endregion + +# region LoRA +alien_lora_sdxl = StarterModel( + name="Alien Style", + base=BaseModelType.StableDiffusionXL, + source="https://huggingface.co/RalFinger/alien-style-lora-sdxl/resolve/main/alienzkin-sdxl.safetensors", + description="Futuristic, intricate alien styles. Trigger with 'alienzkin'.", + type=ModelType.LoRA, +) +noodle_lora_sdxl = StarterModel( + name="Noodles Style", + base=BaseModelType.StableDiffusionXL, + source="https://huggingface.co/RalFinger/noodles-lora-sdxl/resolve/main/noodlez-sdxl.safetensors", + description="Never-ending, no-holds-barred, noodle nightmare. Trigger with 'noodlez'.", + type=ModelType.LoRA, +) +# endregion +# region TI +easy_neg_sd1 = StarterModel( + name="EasyNegative", + base=BaseModelType.StableDiffusion1, + source="https://huggingface.co/embed/EasyNegative/resolve/main/EasyNegative.safetensors", + description="A textual inversion to use in the negative prompt to reduce bad anatomy", + type=ModelType.TextualInversion, +) +# endregion +# region IP Adapter +ip_adapter_sd1 = StarterModel( + name="Standard Reference (IP Adapter)", + base=BaseModelType.StableDiffusion1, + source="https://huggingface.co/InvokeAI/ip_adapter_sd15/resolve/main/ip-adapter_sd15.safetensors", + description="References images with a more generalized/looser degree of precision.", + type=ModelType.IPAdapter, + dependencies=[ip_adapter_sd_image_encoder], + previous_names=["IP Adapter"], +) +ip_adapter_plus_sd1 = StarterModel( + name="Precise Reference (IP Adapter Plus)", + base=BaseModelType.StableDiffusion1, + source="https://huggingface.co/InvokeAI/ip_adapter_plus_sd15/resolve/main/ip-adapter-plus_sd15.safetensors", + description="References images with a higher degree of precision.", + type=ModelType.IPAdapter, + dependencies=[ip_adapter_sd_image_encoder], + previous_names=["IP Adapter Plus"], +) +ip_adapter_plus_face_sd1 = StarterModel( + name="Face Reference (IP Adapter Plus Face)", + base=BaseModelType.StableDiffusion1, + source="https://huggingface.co/InvokeAI/ip_adapter_plus_face_sd15/resolve/main/ip-adapter-plus-face_sd15.safetensors", + description="References images with a higher degree of precision, adapted for faces", + type=ModelType.IPAdapter, + dependencies=[ip_adapter_sd_image_encoder], + previous_names=["IP Adapter Plus Face"], +) +ip_adapter_sdxl = StarterModel( + name="Standard Reference (IP Adapter ViT-H)", + base=BaseModelType.StableDiffusionXL, + source="https://huggingface.co/InvokeAI/ip_adapter_sdxl_vit_h/resolve/main/ip-adapter_sdxl_vit-h.safetensors", + description="References images with a higher degree of precision.", + type=ModelType.IPAdapter, + dependencies=[ip_adapter_sdxl_image_encoder], + previous_names=["IP Adapter SDXL"], +) +ip_adapter_plus_sdxl = StarterModel( + name="Precise Reference (IP Adapter Plus ViT-H)", + base=BaseModelType.StableDiffusionXL, + source="https://huggingface.co/InvokeAI/ip-adapter-plus_sdxl_vit-h/resolve/main/ip-adapter-plus_sdxl_vit-h.safetensors", + description="References images with a higher degree of precision.", + type=ModelType.IPAdapter, + dependencies=[ip_adapter_sdxl_image_encoder], + previous_names=["IP Adapter Plus SDXL"], +) +ip_adapter_flux = StarterModel( + name="Standard Reference (XLabs FLUX IP-Adapter v2)", + base=BaseModelType.Flux, + source="https://huggingface.co/XLabs-AI/flux-ip-adapter-v2/resolve/main/ip_adapter.safetensors", + description="References images with a more generalized/looser degree of precision.", + type=ModelType.IPAdapter, + dependencies=[clip_vit_l_image_encoder], +) +# endregion +# region ControlNet +qr_code_cnet_sd1 = StarterModel( + name="QRCode Monster v2 (SD1.5)", + base=BaseModelType.StableDiffusion1, + source="monster-labs/control_v1p_sd15_qrcode_monster::v2", + description="ControlNet model that generates scannable creative QR codes", + type=ModelType.ControlNet, +) +qr_code_cnet_sdxl = StarterModel( + name="QRCode Monster (SDXL)", + base=BaseModelType.StableDiffusionXL, + source="monster-labs/control_v1p_sdxl_qrcode_monster", + description="ControlNet model that generates scannable creative QR codes", + type=ModelType.ControlNet, +) +canny_sd1 = StarterModel( + name="Hard Edge Detection (canny)", + base=BaseModelType.StableDiffusion1, + source="lllyasviel/control_v11p_sd15_canny", + description="Uses detected edges in the image to control composition.", + type=ModelType.ControlNet, + previous_names=["canny"], +) +inpaint_cnet_sd1 = StarterModel( + name="Inpainting", + base=BaseModelType.StableDiffusion1, + source="lllyasviel/control_v11p_sd15_inpaint", + description="ControlNet weights trained on sd-1.5 with canny conditioning, inpaint version", + type=ModelType.ControlNet, + previous_names=["inpaint"], +) +mlsd_sd1 = StarterModel( + name="Line Drawing (mlsd)", + base=BaseModelType.StableDiffusion1, + source="lllyasviel/control_v11p_sd15_mlsd", + description="Uses straight line detection for controlling the generation.", + type=ModelType.ControlNet, + previous_names=["mlsd"], +) +depth_sd1 = StarterModel( + name="Depth Map", + base=BaseModelType.StableDiffusion1, + source="lllyasviel/control_v11f1p_sd15_depth", + description="Uses depth information in the image to control the depth in the generation.", + type=ModelType.ControlNet, + previous_names=["depth"], +) +normal_bae_sd1 = StarterModel( + name="Lighting Detection (Normals)", + base=BaseModelType.StableDiffusion1, + source="lllyasviel/control_v11p_sd15_normalbae", + description="Uses detected lighting information to guide the lighting of the composition.", + type=ModelType.ControlNet, + previous_names=["normal_bae"], +) +seg_sd1 = StarterModel( + name="Segmentation Map", + base=BaseModelType.StableDiffusion1, + source="lllyasviel/control_v11p_sd15_seg", + description="Uses segmentation maps to guide the structure of the composition.", + type=ModelType.ControlNet, + previous_names=["seg"], +) +lineart_sd1 = StarterModel( + name="Lineart", + base=BaseModelType.StableDiffusion1, + source="lllyasviel/control_v11p_sd15_lineart", + description="Uses lineart detection to guide the lighting of the composition.", + type=ModelType.ControlNet, + previous_names=["lineart"], +) +lineart_anime_sd1 = StarterModel( + name="Lineart Anime", + base=BaseModelType.StableDiffusion1, + source="lllyasviel/control_v11p_sd15s2_lineart_anime", + description="Uses anime lineart detection to guide the lighting of the composition.", + type=ModelType.ControlNet, + previous_names=["lineart_anime"], +) +openpose_sd1 = StarterModel( + name="Pose Detection (openpose)", + base=BaseModelType.StableDiffusion1, + source="lllyasviel/control_v11p_sd15_openpose", + description="Uses pose information to control the pose of human characters in the generation.", + type=ModelType.ControlNet, + previous_names=["openpose"], +) +scribble_sd1 = StarterModel( + name="Contour Detection (scribble)", + base=BaseModelType.StableDiffusion1, + source="lllyasviel/control_v11p_sd15_scribble", + description="Uses edges, contours, or line art in the image to control composition.", + type=ModelType.ControlNet, + previous_names=["scribble"], +) +softedge_sd1 = StarterModel( + name="Soft Edge Detection (softedge)", + base=BaseModelType.StableDiffusion1, + source="lllyasviel/control_v11p_sd15_softedge", + description="Uses a soft edge detection map to control composition.", + type=ModelType.ControlNet, + previous_names=["softedge"], +) +shuffle_sd1 = StarterModel( + name="Remix (shuffle)", + base=BaseModelType.StableDiffusion1, + source="lllyasviel/control_v11e_sd15_shuffle", + description="ControlNet weights trained on sd-1.5 with shuffle image conditioning", + type=ModelType.ControlNet, + previous_names=["shuffle"], +) +tile_sd1 = StarterModel( + name="Tile", + base=BaseModelType.StableDiffusion1, + source="lllyasviel/control_v11f1e_sd15_tile", + description="Uses image data to replicate exact colors/structure in the resulting generation.", + type=ModelType.ControlNet, + previous_names=["tile"], +) +canny_sdxl = StarterModel( + name="Hard Edge Detection (canny)", + base=BaseModelType.StableDiffusionXL, + source="xinsir/controlNet-canny-sdxl-1.0", + description="Uses detected edges in the image to control composition.", + type=ModelType.ControlNet, + previous_names=["canny-sdxl"], +) +depth_sdxl = StarterModel( + name="Depth Map", + base=BaseModelType.StableDiffusionXL, + source="diffusers/controlNet-depth-sdxl-1.0", + description="Uses depth information in the image to control the depth in the generation.", + type=ModelType.ControlNet, + previous_names=["depth-sdxl"], +) +softedge_sdxl = StarterModel( + name="Soft Edge Detection (softedge)", + base=BaseModelType.StableDiffusionXL, + source="SargeZT/controlNet-sd-xl-1.0-softedge-dexined", + description="Uses a soft edge detection map to control composition.", + type=ModelType.ControlNet, + previous_names=["softedge-dexined-sdxl"], +) +openpose_sdxl = StarterModel( + name="Pose Detection (openpose)", + base=BaseModelType.StableDiffusionXL, + source="xinsir/controlNet-openpose-sdxl-1.0", + description="Uses pose information to control the pose of human characters in the generation.", + type=ModelType.ControlNet, + previous_names=["openpose-sdxl", "controlnet-openpose-sdxl"], +) +scribble_sdxl = StarterModel( + name="Contour Detection (scribble)", + base=BaseModelType.StableDiffusionXL, + source="xinsir/controlNet-scribble-sdxl-1.0", + description="Uses edges, contours, or line art in the image to control composition.", + type=ModelType.ControlNet, + previous_names=["scribble-sdxl", "controlnet-scribble-sdxl"], +) +tile_sdxl = StarterModel( + name="Tile", + base=BaseModelType.StableDiffusionXL, + source="xinsir/controlNet-tile-sdxl-1.0", + description="Uses image data to replicate exact colors/structure in the resulting generation.", + type=ModelType.ControlNet, + previous_names=["tile-sdxl"], +) +union_cnet_sdxl = StarterModel( + name="Multi-Guidance Detection (Union Pro)", + base=BaseModelType.StableDiffusionXL, + source="InvokeAI/Xinsir-SDXL_Controlnet_Union", + description="A unified ControlNet for SDXL model that supports 10+ control types", + type=ModelType.ControlNet, +) +union_cnet_flux = StarterModel( + name="FLUX.1-dev-Controlnet-Union", + base=BaseModelType.Flux, + source="InstantX/FLUX.1-dev-Controlnet-Union", + description="A unified ControlNet for FLUX.1-dev model that supports 7 control modes, including canny (0), tile (1), depth (2), blur (3), pose (4), gray (5), low quality (6)", + type=ModelType.ControlNet, +) +# endregion +# region Control LoRA +flux_canny_control_lora = StarterModel( + name="Hard Edge Detection (Canny)", + base=BaseModelType.Flux, + source="black-forest-labs/FLUX.1-Canny-dev-lora::flux1-canny-dev-lora.safetensors", + description="Uses detected edges in the image to control composition.", + type=ModelType.ControlLoRa, +) +flux_depth_control_lora = StarterModel( + name="Depth Map", + base=BaseModelType.Flux, + source="black-forest-labs/FLUX.1-Depth-dev-lora::flux1-depth-dev-lora.safetensors", + description="Uses depth information in the image to control the depth in the generation.", + type=ModelType.ControlLoRa, +) +# endregion +# region T2I Adapter +t2i_canny_sd1 = StarterModel( + name="Hard Edge Detection (canny)", + base=BaseModelType.StableDiffusion1, + source="TencentARC/t2iadapter_canny_sd15v2", + description="Uses detected edges in the image to control composition", + type=ModelType.T2IAdapter, + previous_names=["canny-sd15"], +) +t2i_sketch_sd1 = StarterModel( + name="Sketch", + base=BaseModelType.StableDiffusion1, + source="TencentARC/t2iadapter_sketch_sd15v2", + description="Uses a sketch to control composition", + type=ModelType.T2IAdapter, + previous_names=["sketch-sd15"], +) +t2i_depth_sd1 = StarterModel( + name="Depth Map", + base=BaseModelType.StableDiffusion1, + source="TencentARC/t2iadapter_depth_sd15v2", + description="Uses depth information in the image to control the depth in the generation.", + type=ModelType.T2IAdapter, + previous_names=["depth-sd15"], +) +t2i_canny_sdxl = StarterModel( + name="Hard Edge Detection (canny)", + base=BaseModelType.StableDiffusionXL, + source="TencentARC/t2i-adapter-canny-sdxl-1.0", + description="Uses detected edges in the image to control composition", + type=ModelType.T2IAdapter, + previous_names=["canny-sdxl"], +) +t2i_lineart_sdxl = StarterModel( + name="Lineart", + base=BaseModelType.StableDiffusionXL, + source="TencentARC/t2i-adapter-lineart-sdxl-1.0", + description="Uses lineart detection to guide the lighting of the composition.", + type=ModelType.T2IAdapter, + previous_names=["lineart-sdxl"], +) +t2i_sketch_sdxl = StarterModel( + name="Sketch", + base=BaseModelType.StableDiffusionXL, + source="TencentARC/t2i-adapter-sketch-sdxl-1.0", + description="Uses a sketch to control composition", + type=ModelType.T2IAdapter, + previous_names=["sketch-sdxl"], +) +# endregion +# region SpandrelImageToImage +animesharp_v4_rcan = StarterModel( + name="2x-AnimeSharpV4_RCAN", + base=BaseModelType.Any, + source="https://github.com/Kim2091/Kim2091-Models/releases/download/2x-AnimeSharpV4/2x-AnimeSharpV4_RCAN.safetensors", + description="A 2x upscaling model (optimized for anime images).", + type=ModelType.SpandrelImageToImage, +) + +realesrgan_x4 = StarterModel( + name="RealESRGAN_x4plus", + base=BaseModelType.Any, + source="https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth", + description="A Real-ESRGAN 4x upscaling model (general-purpose).", + type=ModelType.SpandrelImageToImage, +) +esrgan_srx4 = StarterModel( + name="ESRGAN_SRx4_DF2KOST_official", + base=BaseModelType.Any, + source="https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.1/ESRGAN_SRx4_DF2KOST_official-ff704c30.pth", + description="The official ESRGAN 4x upscaling model.", + type=ModelType.SpandrelImageToImage, +) +realesrgan_x2 = StarterModel( + name="RealESRGAN_x2plus", + base=BaseModelType.Any, + source="https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth", + description="A Real-ESRGAN 2x upscaling model (general-purpose).", + type=ModelType.SpandrelImageToImage, +) +swinir = StarterModel( + name="SwinIR - realSR_BSRGAN_DFOWMFC_s64w8_SwinIR-L_x4_GAN", + base=BaseModelType.Any, + source="https://github.com/JingyunLiang/SwinIR/releases/download/v0.0/003_realSR_BSRGAN_DFOWMFC_s64w8_SwinIR-L_x4_GAN-with-dict-keys-params-and-params_ema.pth", + description="A SwinIR 4x upscaling model.", + type=ModelType.SpandrelImageToImage, +) + +# endregion + +# region CogView4 +cogview4 = StarterModel( + name="CogView4", + base=BaseModelType.CogView4, + source="THUDM/CogView4-6B", + description="The base CogView4 model (~31GB).", + type=ModelType.Main, +) +# endregion + +# region Qwen Image components (shared between Edit and txt2img variants) +qwen_image_vae = StarterModel( + name="Qwen Image VAE", + base=BaseModelType.QwenImage, + source="Qwen/Qwen-Image-Edit-2511::vae/diffusion_pytorch_model.safetensors", + description="Qwen Image VAE (AutoencoderKLQwenImage), shared between the Edit and txt2img variants. " + "Use with GGUF transformers to avoid downloading the full ~40GB Diffusers pipeline. (~250MB)", + type=ModelType.VAE, + format=ModelFormat.Checkpoint, +) + +qwen_vl_encoder_fp8 = StarterModel( + name="Qwen2.5-VL Encoder (fp8 scaled)", + base=BaseModelType.Any, + source="https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/text_encoders/qwen_2.5_vl_7b_fp8_scaled.safetensors", + description="ComfyUI's single-file FP8-scaled Qwen2.5-VL 7B encoder. Bundles the language model and " + "visual tower; tokenizer/processor are fetched from HuggingFace on first use. (~7GB)", + type=ModelType.QwenVLEncoder, + format=ModelFormat.Checkpoint, +) + +qwen_vl_encoder_diffusers = StarterModel( + name="Qwen2.5-VL Encoder (Diffusers)", + base=BaseModelType.Any, + source="Qwen/Qwen-Image-Edit-2511::text_encoder+tokenizer+processor", + description="Full-precision Qwen2.5-VL 7B encoder in Diffusers folder layout (text_encoder + tokenizer + processor). " + "Larger than the fp8 variant but no on-the-fly dequantization. (~16GB)", + type=ModelType.QwenVLEncoder, + format=ModelFormat.QwenVLEncoder, +) +# endregion + +# region Qwen Image Edit +qwen_image_edit = StarterModel( + name="Qwen Image Edit 2511", + base=BaseModelType.QwenImage, + source="Qwen/Qwen-Image-Edit-2511", + description="Qwen Image Edit 2511 full diffusers model. Supports text-guided image editing with multiple reference images. (~40GB)", + type=ModelType.Main, + variant=QwenImageVariantType.Edit, +) + +qwen_image_edit_gguf_q4_k_m = StarterModel( + name="Qwen Image Edit 2511 (Q4_K_M)", + base=BaseModelType.QwenImage, + source="https://huggingface.co/unsloth/Qwen-Image-Edit-2511-GGUF/resolve/main/qwen-image-edit-2511-Q4_K_M.gguf", + description="Qwen Image Edit 2511 - Q4_K_M quantized transformer. Good quality/size balance. (~13GB)", + type=ModelType.Main, + format=ModelFormat.GGUFQuantized, + variant=QwenImageVariantType.Edit, + dependencies=[qwen_image_vae, qwen_vl_encoder_fp8], +) + +qwen_image_edit_gguf_q2_k = StarterModel( + name="Qwen Image Edit 2511 (Q2_K)", + base=BaseModelType.QwenImage, + source="https://huggingface.co/unsloth/Qwen-Image-Edit-2511-GGUF/resolve/main/qwen-image-edit-2511-Q2_K.gguf", + description="Qwen Image Edit 2511 - Q2_K heavily quantized transformer. Smallest size, lower quality. (~7.5GB)", + type=ModelType.Main, + format=ModelFormat.GGUFQuantized, + variant=QwenImageVariantType.Edit, + dependencies=[qwen_image_vae, qwen_vl_encoder_fp8], +) + +qwen_image_edit_gguf_q6_k = StarterModel( + name="Qwen Image Edit 2511 (Q6_K)", + base=BaseModelType.QwenImage, + source="https://huggingface.co/unsloth/Qwen-Image-Edit-2511-GGUF/resolve/main/qwen-image-edit-2511-Q6_K.gguf", + description="Qwen Image Edit 2511 - Q6_K quantized transformer. Near-lossless quality. (~17GB)", + type=ModelType.Main, + format=ModelFormat.GGUFQuantized, + variant=QwenImageVariantType.Edit, + dependencies=[qwen_image_vae, qwen_vl_encoder_fp8], +) + +qwen_image_edit_gguf_q8_0 = StarterModel( + name="Qwen Image Edit 2511 (Q8_0)", + base=BaseModelType.QwenImage, + source="https://huggingface.co/unsloth/Qwen-Image-Edit-2511-GGUF/resolve/main/qwen-image-edit-2511-Q8_0.gguf", + description="Qwen Image Edit 2511 - Q8_0 quantized transformer. Highest quality quantization. (~22GB)", + type=ModelType.Main, + format=ModelFormat.GGUFQuantized, + variant=QwenImageVariantType.Edit, + dependencies=[qwen_image_vae, qwen_vl_encoder_fp8], +) + +qwen_image_edit_lightning_4step = StarterModel( + name="Qwen Image Edit Lightning (4-step, bf16)", + base=BaseModelType.QwenImage, + source="https://huggingface.co/lightx2v/Qwen-Image-Edit-2511-Lightning/resolve/main/Qwen-Image-Edit-2511-Lightning-4steps-V1.0-bf16.safetensors", + description="Lightning distillation LoRA for Qwen Image Edit — enables generation in just 4 steps. " + "Settings: Steps=4, CFG=1, Shift Override=3.", + type=ModelType.LoRA, +) + +qwen_image_edit_lightning_8step = StarterModel( + name="Qwen Image Edit Lightning (8-step, bf16)", + base=BaseModelType.QwenImage, + source="https://huggingface.co/lightx2v/Qwen-Image-Edit-2511-Lightning/resolve/main/Qwen-Image-Edit-2511-Lightning-8steps-V1.0-bf16.safetensors", + description="Lightning distillation LoRA for Qwen Image Edit — enables generation in 8 steps with better quality. " + "Settings: Steps=8, CFG=1, Shift Override=3.", + type=ModelType.LoRA, +) + +# Qwen Image (txt2img) +qwen_image = StarterModel( + name="Qwen Image 2512", + base=BaseModelType.QwenImage, + source="Qwen/Qwen-Image-2512", + description="Qwen Image 2512 full diffusers model. High-quality text-to-image generation. (~40GB)", + type=ModelType.Main, +) + +qwen_image_gguf_q4_k_m = StarterModel( + name="Qwen Image 2512 (Q4_K_M)", + base=BaseModelType.QwenImage, + source="https://huggingface.co/unsloth/Qwen-Image-2512-GGUF/resolve/main/qwen-image-2512-Q4_K_M.gguf", + description="Qwen Image 2512 - Q4_K_M quantized transformer. Good quality/size balance. (~13GB)", + type=ModelType.Main, + format=ModelFormat.GGUFQuantized, + dependencies=[qwen_image_vae, qwen_vl_encoder_fp8], +) + +qwen_image_gguf_q2_k = StarterModel( + name="Qwen Image 2512 (Q2_K)", + base=BaseModelType.QwenImage, + source="https://huggingface.co/unsloth/Qwen-Image-2512-GGUF/resolve/main/qwen-image-2512-Q2_K.gguf", + description="Qwen Image 2512 - Q2_K heavily quantized transformer. Smallest size, lower quality. (~7.5GB)", + type=ModelType.Main, + format=ModelFormat.GGUFQuantized, + dependencies=[qwen_image_vae, qwen_vl_encoder_fp8], +) + +qwen_image_gguf_q6_k = StarterModel( + name="Qwen Image 2512 (Q6_K)", + base=BaseModelType.QwenImage, + source="https://huggingface.co/unsloth/Qwen-Image-2512-GGUF/resolve/main/qwen-image-2512-Q6_K.gguf", + description="Qwen Image 2512 - Q6_K quantized transformer. Near-lossless quality. (~17GB)", + type=ModelType.Main, + format=ModelFormat.GGUFQuantized, + dependencies=[qwen_image_vae, qwen_vl_encoder_fp8], +) + +qwen_image_gguf_q8_0 = StarterModel( + name="Qwen Image 2512 (Q8_0)", + base=BaseModelType.QwenImage, + source="https://huggingface.co/unsloth/Qwen-Image-2512-GGUF/resolve/main/qwen-image-2512-Q8_0.gguf", + description="Qwen Image 2512 - Q8_0 quantized transformer. Highest quality quantization. (~22GB)", + type=ModelType.Main, + format=ModelFormat.GGUFQuantized, + dependencies=[qwen_image_vae, qwen_vl_encoder_fp8], +) + +qwen_image_lightning_4step = StarterModel( + name="Qwen Image Lightning (4-step, V2.0, bf16)", + base=BaseModelType.QwenImage, + source="https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-4steps-V2.0-bf16.safetensors", + description="Lightning distillation LoRA for Qwen Image — enables generation in just 4 steps. " + "Settings: Steps=4, CFG=1, Shift Override=3.", + type=ModelType.LoRA, +) + +qwen_image_lightning_8step = StarterModel( + name="Qwen Image Lightning (8-step, V2.0, bf16)", + base=BaseModelType.QwenImage, + source="https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-8steps-V2.0-bf16.safetensors", + description="Lightning distillation LoRA for Qwen Image — enables generation in 8 steps with better quality. " + "Settings: Steps=8, CFG=1, Shift Override=3.", + type=ModelType.LoRA, +) +# endregion + +# region SigLIP +siglip = StarterModel( + name="SigLIP - google/siglip-so400m-patch14-384", + base=BaseModelType.Any, + source="google/siglip-so400m-patch14-384", + description="A SigLIP model (used by FLUX Redux).", + type=ModelType.SigLIP, +) +# endregion + +# region FLUX Redux +flux_redux = StarterModel( + name="FLUX Redux", + base=BaseModelType.Flux, + source="black-forest-labs/FLUX.1-Redux-dev::flux1-redux-dev.safetensors", + description="FLUX Redux model (for image variation).", + type=ModelType.FluxRedux, + dependencies=[siglip], +) +# endregion + +# region LlavaOnevisionModel (vision-language models for Image-to-Prompt) +llava_onevision = StarterModel( + name="LLaVA Onevision Qwen2 0.5B", + base=BaseModelType.Any, + source="llava-hf/llava-onevision-qwen2-0.5b-ov-hf", + description="LLaVA Onevision vision-language model (~1 GB). Lightweight default for the Image-to-Prompt feature.", + type=ModelType.LlavaOnevision, +) + +llava_onevision_7b = StarterModel( + name="LLaVA Onevision Qwen2 7B", + base=BaseModelType.Any, + source="llava-hf/llava-onevision-qwen2-7b-ov-hf", + description="LLaVA Onevision 7B vision-language model. Larger, higher-quality alternative for Image-to-Prompt. (~16 GB)", + type=ModelType.LlavaOnevision, +) +# endregion + +# region TextLLM (causal language models for Prompt Expansion) +qwen2_5_1_5b_instruct = StarterModel( + name="Qwen2.5-1.5B-Instruct", + base=BaseModelType.Any, + source="Qwen/Qwen2.5-1.5B-Instruct", + description="Qwen2.5 1.5B instruction-tuned LLM. Recommended default for the Prompt Expansion feature — small and fast. (~3 GB)", + type=ModelType.TextLLM, +) + +qwen2_5_3b_instruct = StarterModel( + name="Qwen2.5-3B-Instruct", + base=BaseModelType.Any, + source="Qwen/Qwen2.5-3B-Instruct", + description="Qwen2.5 3B instruction-tuned LLM. Better prompt expansion quality at the cost of more VRAM. (~6 GB)", + type=ModelType.TextLLM, +) + +smollm2_1_7b_instruct = StarterModel( + name="SmolLM2-1.7B-Instruct", + base=BaseModelType.Any, + source="HuggingFaceTB/SmolLM2-1.7B-Instruct", + description="SmolLM2 1.7B instruction-tuned LLM (Apache-2.0). Alternative to Qwen for prompt expansion. (~3 GB)", + type=ModelType.TextLLM, +) +# endregion + +# region FLUX Fill +flux_fill = StarterModel( + name="FLUX Fill", + base=BaseModelType.Flux, + source="black-forest-labs/FLUX.1-Fill-dev::flux1-fill-dev.safetensors", + description="FLUX Fill model (for inpainting).", + type=ModelType.Main, +) +# endregion + +# region FLUX.2 Klein +flux2_vae = StarterModel( + name="FLUX.2 VAE", + base=BaseModelType.Flux2, + source="black-forest-labs/FLUX.2-klein-4B::vae", + description="FLUX.2 VAE (16-channel, same architecture as FLUX.1 VAE). ~168MB", + type=ModelType.VAE, +) + +flux2_klein_qwen3_4b_encoder = StarterModel( + name="FLUX.2 Klein Qwen3 4B Encoder", + base=BaseModelType.Any, + source="black-forest-labs/FLUX.2-klein-4B::text_encoder+tokenizer", + description="Qwen3 4B text encoder for FLUX.2 Klein 4B (also compatible with Z-Image). ~8GB", + type=ModelType.Qwen3Encoder, +) + +flux2_klein_qwen3_8b_encoder = StarterModel( + name="FLUX.2 Klein Qwen3 8B Encoder", + base=BaseModelType.Any, + source="black-forest-labs/FLUX.2-klein-9B::text_encoder+tokenizer", + description="Qwen3 8B text encoder for FLUX.2 Klein 9B models. ~16GB", + type=ModelType.Qwen3Encoder, +) + +flux2_klein_4b = StarterModel( + name="FLUX.2 Klein 4B (Diffusers)", + base=BaseModelType.Flux2, + source="black-forest-labs/FLUX.2-klein-4B", + description="FLUX.2 Klein 4B in Diffusers format - includes transformer, VAE and Qwen3 encoder. ~16GB", + type=ModelType.Main, +) + +flux2_klein_4b_single = StarterModel( + name="FLUX.2 Klein 4B", + base=BaseModelType.Flux2, + source="https://huggingface.co/black-forest-labs/FLUX.2-klein-4B/resolve/main/flux-2-klein-4b.safetensors", + description="FLUX.2 Klein 4B standalone transformer. Installs with VAE and Qwen3 4B encoder. ~8GB", + type=ModelType.Main, + dependencies=[flux2_vae, flux2_klein_qwen3_4b_encoder], +) + +flux2_klein_4b_fp8 = StarterModel( + name="FLUX.2 Klein 4B (FP8)", + base=BaseModelType.Flux2, + source="https://huggingface.co/black-forest-labs/FLUX.2-klein-4b-fp8/resolve/main/flux-2-klein-4b-fp8.safetensors", + description="FLUX.2 Klein 4B FP8 quantized - smaller and faster. Installs with VAE and Qwen3 4B encoder. ~4GB", + type=ModelType.Main, + dependencies=[flux2_vae, flux2_klein_qwen3_4b_encoder], +) + +flux2_klein_9b = StarterModel( + name="FLUX.2 Klein 9B (Diffusers)", + base=BaseModelType.Flux2, + source="black-forest-labs/FLUX.2-klein-9B", + description="FLUX.2 Klein 9B in Diffusers format - includes transformer, VAE and Qwen3 encoder. ~35GB", + type=ModelType.Main, +) + +flux2_klein_9b_fp8 = StarterModel( + name="FLUX.2 Klein 9B (FP8)", + base=BaseModelType.Flux2, + source="https://huggingface.co/black-forest-labs/FLUX.2-klein-9b-fp8/resolve/main/flux-2-klein-9b-fp8.safetensors", + description="FLUX.2 Klein 9B FP8 quantized - more efficient than full precision. Installs with VAE and Qwen3 8B encoder. ~9.5GB", + type=ModelType.Main, + dependencies=[flux2_vae, flux2_klein_qwen3_8b_encoder], +) + +flux2_klein_4b_gguf_q4 = StarterModel( + name="FLUX.2 Klein 4B (GGUF Q4)", + base=BaseModelType.Flux2, + source="https://huggingface.co/unsloth/FLUX.2-klein-4B-GGUF/resolve/main/flux-2-klein-4b-Q4_K_M.gguf", + description="FLUX.2 Klein 4B GGUF Q4_K_M quantized - runs on 6-8GB VRAM. Installs with VAE and Qwen3 4B encoder. ~2.6GB", + type=ModelType.Main, + format=ModelFormat.GGUFQuantized, + dependencies=[flux2_vae, flux2_klein_qwen3_4b_encoder], +) + +flux2_klein_4b_gguf_q8 = StarterModel( + name="FLUX.2 Klein 4B (GGUF Q8)", + base=BaseModelType.Flux2, + source="https://huggingface.co/unsloth/FLUX.2-klein-4B-GGUF/resolve/main/flux-2-klein-4b-Q8_0.gguf", + description="FLUX.2 Klein 4B GGUF Q8_0 quantized - higher quality than Q4. Installs with VAE and Qwen3 4B encoder. ~4.3GB", + type=ModelType.Main, + format=ModelFormat.GGUFQuantized, + dependencies=[flux2_vae, flux2_klein_qwen3_4b_encoder], +) + +flux2_klein_9b_gguf_q4 = StarterModel( + name="FLUX.2 Klein 9B (GGUF Q4)", + base=BaseModelType.Flux2, + source="https://huggingface.co/unsloth/FLUX.2-klein-9B-GGUF/resolve/main/flux-2-klein-9b-Q4_K_M.gguf", + description="FLUX.2 Klein 9B GGUF Q4_K_M quantized - runs on 12GB+ VRAM. Installs with VAE and Qwen3 8B encoder. ~5.8GB", + type=ModelType.Main, + format=ModelFormat.GGUFQuantized, + dependencies=[flux2_vae, flux2_klein_qwen3_8b_encoder], +) + +flux2_klein_9b_gguf_q8 = StarterModel( + name="FLUX.2 Klein 9B (GGUF Q8)", + base=BaseModelType.Flux2, + source="https://huggingface.co/unsloth/FLUX.2-klein-9B-GGUF/resolve/main/flux-2-klein-9b-Q8_0.gguf", + description="FLUX.2 Klein 9B GGUF Q8_0 quantized - higher quality than Q4. Installs with VAE and Qwen3 8B encoder. ~10GB", + type=ModelType.Main, + format=ModelFormat.GGUFQuantized, + dependencies=[flux2_vae, flux2_klein_qwen3_8b_encoder], +) +# endregion + +# region Z-Image +z_image_qwen3_encoder = StarterModel( + name="Z-Image Qwen3 Text Encoder", + base=BaseModelType.Any, + source="Tongyi-MAI/Z-Image-Turbo::text_encoder+tokenizer", + description="Qwen3 4B text encoder with tokenizer for Z-Image (full precision). ~8GB", + type=ModelType.Qwen3Encoder, +) + +z_image_qwen3_encoder_quantized = StarterModel( + name="Z-Image Qwen3 Text Encoder (quantized)", + base=BaseModelType.Any, + source="https://huggingface.co/worstplayer/Z-Image_Qwen_3_4b_text_encoder_GGUF/resolve/main/Qwen_3_4b-Q6_K.gguf", + description="Qwen3 4B text encoder for Z-Image quantized to GGUF Q6_K format. ~3.3GB", + type=ModelType.Qwen3Encoder, + format=ModelFormat.GGUFQuantized, +) + +z_image_turbo = StarterModel( + name="Z-Image Turbo", + base=BaseModelType.ZImage, + source="Tongyi-MAI/Z-Image-Turbo", + description="Z-Image Turbo - fast 6B parameter text-to-image model with 8 inference steps. Supports bilingual prompts (English & Chinese). ~33GB", + type=ModelType.Main, +) + +z_image_turbo_quantized = StarterModel( + name="Z-Image Turbo (quantized)", + base=BaseModelType.ZImage, + source="https://huggingface.co/leejet/Z-Image-Turbo-GGUF/resolve/main/z_image_turbo-Q4_K.gguf", + description="Z-Image Turbo quantized to GGUF Q4_K format. Requires standalone Qwen3 text encoder and Flux VAE. ~4GB", + type=ModelType.Main, + format=ModelFormat.GGUFQuantized, + dependencies=[z_image_qwen3_encoder_quantized, flux_vae], +) + +z_image_turbo_q8 = StarterModel( + name="Z-Image Turbo (Q8)", + base=BaseModelType.ZImage, + source="https://huggingface.co/leejet/Z-Image-Turbo-GGUF/resolve/main/z_image_turbo-Q8_0.gguf", + description="Z-Image Turbo quantized to GGUF Q8_0 format. Higher quality, larger size. Requires standalone Qwen3 text encoder and Flux VAE. ~6.6GB", + type=ModelType.Main, + format=ModelFormat.GGUFQuantized, + dependencies=[z_image_qwen3_encoder_quantized, flux_vae], +) + +z_image_controlnet_union = StarterModel( + name="Z-Image ControlNet Union", + base=BaseModelType.ZImage, + source="https://huggingface.co/alibaba-pai/Z-Image-Turbo-Fun-Controlnet-Union-2.1/resolve/main/Z-Image-Turbo-Fun-Controlnet-Union-2.1-8steps.safetensors", + description="Unified ControlNet for Z-Image Turbo supporting Canny, HED, Depth, Pose, MLSD, and Inpainting modes.", + type=ModelType.ControlNet, +) + +z_image_controlnet_tile = StarterModel( + name="Z-Image ControlNet Tile", + base=BaseModelType.ZImage, + source="https://huggingface.co/alibaba-pai/Z-Image-Turbo-Fun-Controlnet-Union-2.1/resolve/main/Z-Image-Turbo-Fun-Controlnet-Tile-2.1-8steps.safetensors", + description="Dedicated Tile ControlNet for Z-Image Turbo. Useful for upscaling and adding detail. ~6.7GB", + type=ModelType.ControlNet, +) +# endregion + +# region External API +GEMINI_3_IMAGE_ALLOWED_ASPECT_RATIOS = [ + "1:1", + "1:4", + "1:8", + "2:3", + "3:2", + "3:4", + "4:1", + "4:3", + "4:5", + "5:4", + "8:1", + "9:16", + "16:9", + "21:9", +] +GEMINI_3_IMAGE_MAX_SIZE = ExternalImageSize(width=4096, height=4096) + + +def _gemini_3_resolution_presets( + image_sizes: list[str], + aspect_ratios: list[str] | None = None, +) -> list[ExternalResolutionPreset]: + """Build resolution presets for Gemini 3 models. + + Each preset combines an aspect ratio with an image size preset (512/1K/2K/4K). + Pixel dimensions are approximations based on the preset name (longest side). + """ + if aspect_ratios is None: + aspect_ratios = GEMINI_3_IMAGE_ALLOWED_ASPECT_RATIOS + base_pixels = {"512": 512, "1K": 1024, "2K": 2048, "4K": 4096} + presets: list[ExternalResolutionPreset] = [] + for image_size in image_sizes: + base = base_pixels[image_size] + for ratio_str in aspect_ratios: + w_part, h_part = (int(x) for x in ratio_str.split(":")) + if w_part >= h_part: + w = base + h = max(1, round(base * h_part / w_part)) + else: + h = base + w = max(1, round(base * w_part / h_part)) + presets.append( + ExternalResolutionPreset( + label=f"{ratio_str} ({image_size}) — {w}\u00d7{h}", + aspect_ratio=ratio_str, + image_size=image_size, + width=w, + height=h, + ) + ) + return presets + + +GEMINI_3_PRO_RESOLUTION_PRESETS = _gemini_3_resolution_presets(["1K", "2K", "4K"]) +GEMINI_3_1_FLASH_RESOLUTION_PRESETS = _gemini_3_resolution_presets(["512", "1K", "2K", "4K"]) + +gemini_flash_image = StarterModel( + name="Gemini 2.5 Flash Image", + base=BaseModelType.External, + source="external://gemini/gemini-2.5-flash-image", + description="Google Gemini 2.5 Flash image generation model (external API). Requires a configured Gemini API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img"], + supports_seed=True, + supports_reference_images=True, + max_images_per_request=1, + allowed_aspect_ratios=[ + "1:1", + "2:3", + "3:2", + "3:4", + "4:3", + "4:5", + "5:4", + "9:16", + "16:9", + "21:9", + ], + aspect_ratio_sizes={ + "1:1": ExternalImageSize(width=1024, height=1024), + "2:3": ExternalImageSize(width=832, height=1248), + "3:2": ExternalImageSize(width=1248, height=832), + "3:4": ExternalImageSize(width=864, height=1184), + "4:3": ExternalImageSize(width=1184, height=864), + "4:5": ExternalImageSize(width=896, height=1152), + "5:4": ExternalImageSize(width=1152, height=896), + "9:16": ExternalImageSize(width=768, height=1344), + "16:9": ExternalImageSize(width=1344, height=768), + "21:9": ExternalImageSize(width=1536, height=672), + }, + ), + default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), + panel_schema=ExternalModelPanelSchema(prompts=[{"name": "reference_images"}], image=[{"name": "dimensions"}]), +) +gemini_pro_image_preview = StarterModel( + name="Gemini 3 Pro Image Preview", + base=BaseModelType.External, + source="external://gemini/gemini-3-pro-image-preview", + description="Google Gemini 3 Pro image generation preview model (external API). Supports up to 14 reference images, including up to 6 object references and up to 5 character references. Supports 1K/2K/4K resolution presets. Requires a configured Gemini API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img"], + supports_seed=True, + supports_reference_images=True, + max_reference_images=14, + max_images_per_request=1, + max_image_size=GEMINI_3_IMAGE_MAX_SIZE, + allowed_aspect_ratios=GEMINI_3_IMAGE_ALLOWED_ASPECT_RATIOS, + resolution_presets=GEMINI_3_PRO_RESOLUTION_PRESETS, + ), + default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), + panel_schema=ExternalModelPanelSchema(prompts=[{"name": "reference_images"}], image=[{"name": "dimensions"}]), +) +gemini_3_1_flash_image_preview = StarterModel( + name="Gemini 3.1 Flash Image Preview", + base=BaseModelType.External, + source="external://gemini/gemini-3.1-flash-image-preview", + description="Google Gemini 3.1 Flash image generation preview model (external API). Supports up to 14 reference images, including up to 10 object references and up to 4 character references. Supports 512/1K/2K/4K resolution presets. Requires a configured Gemini API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img"], + supports_seed=True, + supports_reference_images=True, + max_reference_images=14, + max_images_per_request=1, + max_image_size=GEMINI_3_IMAGE_MAX_SIZE, + allowed_aspect_ratios=GEMINI_3_IMAGE_ALLOWED_ASPECT_RATIOS, + resolution_presets=GEMINI_3_1_FLASH_RESOLUTION_PRESETS, + ), + default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), + panel_schema=ExternalModelPanelSchema(prompts=[{"name": "reference_images"}], image=[{"name": "dimensions"}]), +) +QWEN_IMAGE_2_ALLOWED_ASPECT_RATIOS = ["1:1", "4:3", "3:4", "16:9", "9:16"] +QWEN_IMAGE_MAX_ALLOWED_ASPECT_RATIOS = ["1:1", "4:3", "3:4", "16:9", "9:16"] +WAN_V2_ALLOWED_ASPECT_RATIOS = ["1:1", "4:3", "3:4", "16:9", "9:16"] + +alibabacloud_qwen_image_2_pro = StarterModel( + name="Qwen Image 2.0 Pro", + base=BaseModelType.External, + source="external://alibabacloud/qwen-image-2.0-pro", + description="Alibaba Cloud Qwen Image 2.0 Pro model (external API). Best quality text-to-image with excellent bilingual text rendering. Requires a configured Alibaba Cloud DashScope API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img"], + supports_negative_prompt=False, + supports_seed=True, + max_images_per_request=4, + allowed_aspect_ratios=QWEN_IMAGE_2_ALLOWED_ASPECT_RATIOS, + aspect_ratio_sizes={ + "1:1": ExternalImageSize(width=2048, height=2048), + "4:3": ExternalImageSize(width=2368, height=1728), + "3:4": ExternalImageSize(width=1728, height=2368), + "16:9": ExternalImageSize(width=2688, height=1536), + "9:16": ExternalImageSize(width=1536, height=2688), + }, + ), + default_settings=ExternalApiModelDefaultSettings(width=2048, height=2048, num_images=1), + panel_schema=ExternalModelPanelSchema(image=[{"name": "dimensions"}]), +) +alibabacloud_qwen_image_2 = StarterModel( + name="Qwen Image 2.0", + base=BaseModelType.External, + source="external://alibabacloud/qwen-image-2.0", + description="Alibaba Cloud Qwen Image 2.0 model (external API). Fast text-to-image with good bilingual text rendering. Requires a configured Alibaba Cloud DashScope API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img"], + supports_negative_prompt=False, + supports_seed=True, + max_images_per_request=4, + allowed_aspect_ratios=QWEN_IMAGE_2_ALLOWED_ASPECT_RATIOS, + aspect_ratio_sizes={ + "1:1": ExternalImageSize(width=2048, height=2048), + "4:3": ExternalImageSize(width=2368, height=1728), + "3:4": ExternalImageSize(width=1728, height=2368), + "16:9": ExternalImageSize(width=2688, height=1536), + "9:16": ExternalImageSize(width=1536, height=2688), + }, + ), + default_settings=ExternalApiModelDefaultSettings(width=2048, height=2048, num_images=1), + panel_schema=ExternalModelPanelSchema(image=[{"name": "dimensions"}]), +) +alibabacloud_qwen_image_max = StarterModel( + name="Qwen Image Max", + base=BaseModelType.External, + source="external://alibabacloud/qwen-image-max", + description="Alibaba Cloud Qwen Image Max model (external API). High quality text-to-image generation. Requires a configured Alibaba Cloud DashScope API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img"], + supports_negative_prompt=False, + supports_seed=True, + max_images_per_request=4, + allowed_aspect_ratios=QWEN_IMAGE_MAX_ALLOWED_ASPECT_RATIOS, + aspect_ratio_sizes={ + "1:1": ExternalImageSize(width=1328, height=1328), + "4:3": ExternalImageSize(width=1472, height=1104), + "3:4": ExternalImageSize(width=1104, height=1472), + "16:9": ExternalImageSize(width=1664, height=928), + "9:16": ExternalImageSize(width=928, height=1664), + }, + ), + default_settings=ExternalApiModelDefaultSettings(width=1328, height=1328, num_images=1), + panel_schema=ExternalModelPanelSchema(image=[{"name": "dimensions"}]), +) +alibabacloud_wan26_t2i = StarterModel( + name="Wan 2.6 Text-to-Image", + base=BaseModelType.External, + source="external://alibabacloud/wan2.6-t2i", + description="Alibaba Cloud Wan 2.6 text-to-image model (external API). Photorealistic image generation. Requires a configured Alibaba Cloud DashScope API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img"], + supports_negative_prompt=False, + supports_seed=True, + max_images_per_request=4, + allowed_aspect_ratios=WAN_V2_ALLOWED_ASPECT_RATIOS, + aspect_ratio_sizes={ + "1:1": ExternalImageSize(width=1024, height=1024), + "4:3": ExternalImageSize(width=1440, height=1080), + "3:4": ExternalImageSize(width=1080, height=1440), + "16:9": ExternalImageSize(width=1440, height=810), + "9:16": ExternalImageSize(width=810, height=1440), + }, + ), + default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), + panel_schema=ExternalModelPanelSchema(image=[{"name": "dimensions"}]), +) +alibabacloud_qwen_image_edit_max = StarterModel( + name="Qwen Image Edit Max", + base=BaseModelType.External, + source="external://alibabacloud/qwen-image-edit-max", + description="Alibaba Cloud Qwen Image Edit Max model (external API). Image editing with industrial design and geometric reasoning, driven by up to 3 reference images. Requires a configured Alibaba Cloud DashScope API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img"], + supports_negative_prompt=False, + supports_reference_images=True, + supports_seed=True, + max_reference_images=3, + max_images_per_request=4, + allowed_aspect_ratios=QWEN_IMAGE_2_ALLOWED_ASPECT_RATIOS, + aspect_ratio_sizes={ + "1:1": ExternalImageSize(width=2048, height=2048), + "4:3": ExternalImageSize(width=2368, height=1728), + "3:4": ExternalImageSize(width=1728, height=2368), + "16:9": ExternalImageSize(width=2688, height=1536), + "9:16": ExternalImageSize(width=1536, height=2688), + }, + ), + default_settings=ExternalApiModelDefaultSettings(width=2048, height=2048, num_images=1), + panel_schema=ExternalModelPanelSchema(prompts=[{"name": "reference_images"}], image=[{"name": "dimensions"}]), +) +OPENAI_GPT_IMAGE_ASPECT_RATIOS = ["1:1", "3:2", "2:3"] +OPENAI_GPT_IMAGE_ASPECT_RATIO_SIZES = { + "1:1": ExternalImageSize(width=1024, height=1024), + "3:2": ExternalImageSize(width=1536, height=1024), + "2:3": ExternalImageSize(width=1024, height=1536), +} +OPENAI_GPT_IMAGE_PANEL_SCHEMA = ExternalModelPanelSchema( + prompts=[{"name": "reference_images"}], image=[{"name": "dimensions"}] +) + +openai_gpt_image_2 = StarterModel( + name="GPT Image 2", + base=BaseModelType.External, + source="external://openai/gpt-image-2", + description="OpenAI GPT-Image-2 image generation model. State-of-the-art image generation and editing with flexible sizing and high-fidelity image inputs. Does not support transparent backgrounds or configurable input fidelity. Requires a configured OpenAI API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img", "img2img"], + supports_reference_images=True, + max_images_per_request=10, + allowed_aspect_ratios=OPENAI_GPT_IMAGE_ASPECT_RATIOS, + aspect_ratio_sizes=OPENAI_GPT_IMAGE_ASPECT_RATIO_SIZES, + ), + default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), + panel_schema=OPENAI_GPT_IMAGE_PANEL_SCHEMA, +) +openai_gpt_image_1_5 = StarterModel( + name="GPT Image 1.5", + base=BaseModelType.External, + source="external://openai/gpt-image-1.5", + description="OpenAI GPT-Image-1.5 image generation model. Fastest and most affordable GPT image model. Requires a configured OpenAI API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img", "img2img"], + supports_reference_images=True, + max_images_per_request=10, + allowed_aspect_ratios=OPENAI_GPT_IMAGE_ASPECT_RATIOS, + aspect_ratio_sizes=OPENAI_GPT_IMAGE_ASPECT_RATIO_SIZES, + ), + default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), + panel_schema=OPENAI_GPT_IMAGE_PANEL_SCHEMA, +) +openai_gpt_image_1 = StarterModel( + name="GPT Image 1", + base=BaseModelType.External, + source="external://openai/gpt-image-1", + description="OpenAI GPT-Image-1 image generation model. High quality image generation. Requires a configured OpenAI API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img", "img2img"], + supports_reference_images=True, + max_images_per_request=10, + allowed_aspect_ratios=OPENAI_GPT_IMAGE_ASPECT_RATIOS, + aspect_ratio_sizes=OPENAI_GPT_IMAGE_ASPECT_RATIO_SIZES, + ), + default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), + panel_schema=OPENAI_GPT_IMAGE_PANEL_SCHEMA, +) +openai_gpt_image_1_mini = StarterModel( + name="GPT Image 1 Mini", + base=BaseModelType.External, + source="external://openai/gpt-image-1-mini", + description="OpenAI GPT-Image-1-Mini image generation model. Cost-efficient option, 80%% cheaper than GPT-Image-1. Requires a configured OpenAI API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img", "img2img"], + supports_reference_images=True, + max_images_per_request=10, + allowed_aspect_ratios=OPENAI_GPT_IMAGE_ASPECT_RATIOS, + aspect_ratio_sizes=OPENAI_GPT_IMAGE_ASPECT_RATIO_SIZES, + ), + default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), + panel_schema=OPENAI_GPT_IMAGE_PANEL_SCHEMA, +) +openai_dall_e_3 = StarterModel( + name="DALL-E 3", + base=BaseModelType.External, + source="external://openai/dall-e-3", + description="OpenAI DALL-E 3 image generation model. Supports vivid and natural styles. Only text-to-image, no editing. Requires a configured OpenAI API key and may incur provider usage costs.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img"], + max_images_per_request=1, + allowed_aspect_ratios=["1:1", "7:4", "4:7"], + aspect_ratio_sizes={ + "1:1": ExternalImageSize(width=1024, height=1024), + "7:4": ExternalImageSize(width=1792, height=1024), + "4:7": ExternalImageSize(width=1024, height=1792), + }, + ), + default_settings=ExternalApiModelDefaultSettings(width=1024, height=1024, num_images=1), + panel_schema=ExternalModelPanelSchema(image=[{"name": "dimensions"}]), +) +SEEDREAM_ASPECT_RATIOS = ["1:1", "2:3", "3:2", "3:4", "4:3", "9:16", "16:9", "21:9"] +SEEDREAM_2K_SIZES = { + "1:1": ExternalImageSize(width=2048, height=2048), + "3:4": ExternalImageSize(width=1728, height=2304), + "4:3": ExternalImageSize(width=2304, height=1728), + "16:9": ExternalImageSize(width=2848, height=1600), + "9:16": ExternalImageSize(width=1600, height=2848), + "3:2": ExternalImageSize(width=2496, height=1664), + "2:3": ExternalImageSize(width=1664, height=2496), + "21:9": ExternalImageSize(width=3136, height=1344), +} +SEEDREAM_1K_SIZES = { + "1:1": ExternalImageSize(width=1024, height=1024), + "3:4": ExternalImageSize(width=864, height=1152), + "4:3": ExternalImageSize(width=1152, height=864), + "16:9": ExternalImageSize(width=1312, height=736), + "9:16": ExternalImageSize(width=736, height=1312), + "2:3": ExternalImageSize(width=832, height=1248), + "3:2": ExternalImageSize(width=1248, height=832), + "21:9": ExternalImageSize(width=1568, height=672), +} +SEEDREAM_PANEL_SCHEMA = ExternalModelPanelSchema(prompts=[{"name": "reference_images"}], image=[{"name": "dimensions"}]) +seedream_5_0 = StarterModel( + name="Seedream 5.0", + base=BaseModelType.External, + source="external://seedream/seedream-5-0-260128", + description="BytePlus Seedream 5.0 flagship image generation model (external API). Supports 2K and 4K resolutions, txt2img and img2img with multi-image reference input.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img", "img2img"], + supports_reference_images=True, + max_reference_images=14, + max_images_per_request=15, + allowed_aspect_ratios=SEEDREAM_ASPECT_RATIOS, + aspect_ratio_sizes=SEEDREAM_2K_SIZES, + ), + default_settings=ExternalApiModelDefaultSettings(width=2048, height=2048, num_images=1), + panel_schema=SEEDREAM_PANEL_SCHEMA, +) +seedream_5_0_lite = StarterModel( + name="Seedream 5.0 Lite", + base=BaseModelType.External, + source="external://seedream/seedream-5-0-lite-260128", + description="BytePlus Seedream 5.0 Lite image generation model (external API). Supports 2K and 4K resolutions, txt2img and img2img with multi-image reference input.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img", "img2img"], + supports_reference_images=True, + max_reference_images=14, + max_images_per_request=15, + allowed_aspect_ratios=SEEDREAM_ASPECT_RATIOS, + aspect_ratio_sizes=SEEDREAM_2K_SIZES, + ), + default_settings=ExternalApiModelDefaultSettings(width=2048, height=2048, num_images=1), + panel_schema=SEEDREAM_PANEL_SCHEMA, +) +seedream_4_5 = StarterModel( + name="Seedream 4.5", + base=BaseModelType.External, + source="external://seedream/seedream-4-5-251128", + description="BytePlus Seedream 4.5 image generation model (external API). Supports 2K and 4K resolutions, txt2img, img2img, batch generation, and multi-image reference input.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img", "img2img"], + supports_reference_images=True, + max_reference_images=14, + max_images_per_request=15, + allowed_aspect_ratios=SEEDREAM_ASPECT_RATIOS, + aspect_ratio_sizes=SEEDREAM_2K_SIZES, + ), + default_settings=ExternalApiModelDefaultSettings(width=2048, height=2048, num_images=1), + panel_schema=SEEDREAM_PANEL_SCHEMA, +) +seedream_4_0 = StarterModel( + name="Seedream 4.0", + base=BaseModelType.External, + source="external://seedream/seedream-4-0-250828", + description="BytePlus Seedream 4.0 image generation model (external API). Supports 1K, 2K, and 4K resolutions, txt2img, img2img, batch generation, and multi-image reference input.", + type=ModelType.ExternalImageGenerator, + format=ModelFormat.ExternalApi, + capabilities=ExternalModelCapabilities( + modes=["txt2img", "img2img"], + supports_reference_images=True, + max_reference_images=14, + max_images_per_request=15, + allowed_aspect_ratios=SEEDREAM_ASPECT_RATIOS, + aspect_ratio_sizes=SEEDREAM_2K_SIZES, + ), + default_settings=ExternalApiModelDefaultSettings(width=2048, height=2048, num_images=1), + panel_schema=SEEDREAM_PANEL_SCHEMA, +) +# Seedream 3.0 T2I (seedream-3-0-t2i-250415) removed — deprecated by BytePlus, replaced by seedream-4-0-250828. + +# DALL-E 2 removed — deprecated by OpenAI, shutdown May 12, 2026. +# region Anima +anima_qwen3_encoder = StarterModel( + name="Anima Qwen3 0.6B Text Encoder", + base=BaseModelType.Any, + source="https://huggingface.co/circlestone-labs/Anima/resolve/main/split_files/text_encoders/qwen_3_06b_base.safetensors", + description="Qwen3 0.6B text encoder for Anima. ~1.2GB", + type=ModelType.Qwen3Encoder, + format=ModelFormat.Checkpoint, +) + +anima_vae = StarterModel( + name="Anima QwenImage VAE", + base=BaseModelType.Anima, + source="https://huggingface.co/circlestone-labs/Anima/resolve/main/split_files/vae/qwen_image_vae.safetensors", + description="QwenImage VAE for Anima (fine-tuned Wan 2.1 VAE, 16 latent channels). ~200MB", + type=ModelType.VAE, + format=ModelFormat.Checkpoint, +) + +anima_base = StarterModel( + name="Anima Base 1.0", + base=BaseModelType.Anima, + source="https://huggingface.co/circlestone-labs/Anima/resolve/main/split_files/diffusion_models/anima-base-v1.0.safetensors", + description="Anima Base 1.0 - 2B parameter anime-focused text-to-image model built on Cosmos Predict2 DiT. ~4.5GB", + type=ModelType.Main, + format=ModelFormat.Checkpoint, + dependencies=[anima_qwen3_encoder, anima_vae], +) +# endregion + +# List of starter models, displayed on the frontend. +# The order/sort of this list is not changed by the frontend - set it how you want it here. +STARTER_MODELS: list[StarterModel] = [ + flux_kontext_quantized, + flux_schnell_quantized, + flux_dev_quantized, + flux_schnell, + flux_dev, + sd35_medium, + sd35_large, + cyberrealistic_sd1, + rev_animated_sd1, + dreamshaper_8_sd1, + dreamshaper_8_inpainting_sd1, + deliberate_sd1, + deliberate_inpainting_sd1, + juggernaut_sdxl, + dreamshaper_sdxl, + archvis_sdxl, + sdxl_refiner, + sdxl_fp16_vae_fix, + flux_vae, + alien_lora_sdxl, + noodle_lora_sdxl, + easy_neg_sd1, + ip_adapter_sd1, + ip_adapter_plus_sd1, + ip_adapter_plus_face_sd1, + ip_adapter_sdxl, + ip_adapter_plus_sdxl, + ip_adapter_flux, + qr_code_cnet_sd1, + qr_code_cnet_sdxl, + canny_sd1, + inpaint_cnet_sd1, + mlsd_sd1, + depth_sd1, + normal_bae_sd1, + seg_sd1, + lineart_sd1, + lineart_anime_sd1, + openpose_sd1, + scribble_sd1, + softedge_sd1, + shuffle_sd1, + tile_sd1, + canny_sdxl, + depth_sdxl, + softedge_sdxl, + openpose_sdxl, + scribble_sdxl, + tile_sdxl, + union_cnet_sdxl, + union_cnet_flux, + flux_canny_control_lora, + flux_depth_control_lora, + t2i_canny_sd1, + t2i_sketch_sd1, + t2i_depth_sd1, + t2i_canny_sdxl, + t2i_lineart_sdxl, + t2i_sketch_sdxl, + realesrgan_x4, + animesharp_v4_rcan, + realesrgan_x2, + swinir, + t5_base_encoder, + t5_8b_quantized_encoder, + clip_l_encoder, + siglip, + flux_redux, + llava_onevision, + llava_onevision_7b, + qwen2_5_1_5b_instruct, + qwen2_5_3b_instruct, + smollm2_1_7b_instruct, + flux_fill, + flux2_vae, + flux2_klein_4b, + flux2_klein_4b_single, + flux2_klein_4b_fp8, + flux2_klein_9b, + flux2_klein_9b_fp8, + flux2_klein_4b_gguf_q4, + flux2_klein_4b_gguf_q8, + flux2_klein_9b_gguf_q4, + flux2_klein_9b_gguf_q8, + flux2_klein_qwen3_4b_encoder, + flux2_klein_qwen3_8b_encoder, + cogview4, + qwen_image_vae, + qwen_vl_encoder_fp8, + qwen_vl_encoder_diffusers, + qwen_image_edit, + qwen_image_edit_gguf_q2_k, + qwen_image_edit_gguf_q4_k_m, + qwen_image_edit_gguf_q6_k, + qwen_image_edit_gguf_q8_0, + qwen_image_edit_lightning_4step, + qwen_image_edit_lightning_8step, + qwen_image, + qwen_image_gguf_q2_k, + qwen_image_gguf_q4_k_m, + qwen_image_gguf_q6_k, + qwen_image_gguf_q8_0, + qwen_image_lightning_4step, + qwen_image_lightning_8step, + flux_krea, + flux_krea_quantized, + z_image_turbo, + z_image_turbo_quantized, + z_image_turbo_q8, + z_image_qwen3_encoder, + z_image_qwen3_encoder_quantized, + z_image_controlnet_union, + z_image_controlnet_tile, + gemini_flash_image, + gemini_pro_image_preview, + gemini_3_1_flash_image_preview, + openai_gpt_image_2, + openai_gpt_image_1_5, + openai_gpt_image_1, + openai_gpt_image_1_mini, + openai_dall_e_3, + seedream_5_0, + seedream_5_0_lite, + seedream_4_5, + seedream_4_0, + alibabacloud_qwen_image_2_pro, + alibabacloud_qwen_image_2, + alibabacloud_qwen_image_max, + alibabacloud_wan26_t2i, + alibabacloud_qwen_image_edit_max, + anima_base, + anima_qwen3_encoder, + anima_vae, +] + +sd1_bundle: list[StarterModel] = [ + dreamshaper_8_sd1, + easy_neg_sd1, + ip_adapter_sd1, + ip_adapter_plus_sd1, + ip_adapter_plus_face_sd1, + canny_sd1, + inpaint_cnet_sd1, + mlsd_sd1, + depth_sd1, + normal_bae_sd1, + seg_sd1, + lineart_sd1, + lineart_anime_sd1, + openpose_sd1, + scribble_sd1, + softedge_sd1, + shuffle_sd1, + tile_sd1, + swinir, +] + +sdxl_bundle: list[StarterModel] = [ + juggernaut_sdxl, + sdxl_fp16_vae_fix, + ip_adapter_sdxl, + ip_adapter_plus_sdxl, + canny_sdxl, + depth_sdxl, + softedge_sdxl, + openpose_sdxl, + scribble_sdxl, + tile_sdxl, + swinir, +] + +flux_bundle: list[StarterModel] = [ + flux_schnell_quantized, + flux_dev_quantized, + flux_vae, + t5_8b_quantized_encoder, + clip_l_encoder, + union_cnet_flux, + ip_adapter_flux, + flux_canny_control_lora, + flux_depth_control_lora, + flux_redux, + flux_fill, + flux_kontext_quantized, + flux_krea_quantized, +] + +zimage_bundle: list[StarterModel] = [ + z_image_turbo_quantized, + z_image_qwen3_encoder_quantized, + z_image_controlnet_union, + z_image_controlnet_tile, + flux_vae, +] + +flux2_klein_bundle: list[StarterModel] = [ + flux2_klein_4b_gguf_q4, + flux2_vae, + flux2_klein_qwen3_4b_encoder, +] + +qwen_image_bundle: list[StarterModel] = [ + qwen_image_vae, + qwen_vl_encoder_fp8, + qwen_image_edit, + qwen_image_edit_gguf_q4_k_m, + qwen_image_edit_gguf_q8_0, + qwen_image_edit_lightning_4step, + qwen_image_edit_lightning_8step, + qwen_image, + qwen_image_gguf_q4_k_m, + qwen_image_gguf_q8_0, + qwen_image_lightning_4step, + qwen_image_lightning_8step, +] + +anima_bundle: list[StarterModel] = [ + anima_base, + anima_qwen3_encoder, + anima_vae, +] + +STARTER_BUNDLES: dict[str, StarterModelBundle] = { + BaseModelType.StableDiffusion1: StarterModelBundle(name="Stable Diffusion 1.5", models=sd1_bundle), + BaseModelType.StableDiffusionXL: StarterModelBundle(name="SDXL", models=sdxl_bundle), + BaseModelType.Flux: StarterModelBundle(name="FLUX.1 dev", models=flux_bundle), + BaseModelType.Flux2: StarterModelBundle(name="FLUX.2 Klein", models=flux2_klein_bundle), + BaseModelType.ZImage: StarterModelBundle(name="Z-Image Turbo", models=zimage_bundle), + BaseModelType.QwenImage: StarterModelBundle(name="Qwen Image", models=qwen_image_bundle), + BaseModelType.Anima: StarterModelBundle(name="Anima", models=anima_bundle), +} + +assert len(STARTER_MODELS) == len({m.source for m in STARTER_MODELS}), "Duplicate starter models" diff --git a/invokeai/backend/model_manager/taxonomy.py b/invokeai/backend/model_manager/taxonomy.py new file mode 100644 index 00000000000..a2e4e58bdc4 --- /dev/null +++ b/invokeai/backend/model_manager/taxonomy.py @@ -0,0 +1,269 @@ +from enum import Enum +from typing import Dict, TypeAlias, Union + +import onnxruntime as ort +import torch +from diffusers.models.modeling_utils import ModelMixin +from diffusers.pipelines.pipeline_utils import DiffusionPipeline +from pydantic import TypeAdapter + +from invokeai.backend.raw_model import RawModel + +# ModelMixin is the base class for all diffusers and transformers models +# RawModel is the InvokeAI wrapper class for ip_adapters, loras, textual_inversion and onnx runtime +AnyModel: TypeAlias = Union[ + ModelMixin, + RawModel, + torch.nn.Module, + Dict[str, torch.Tensor], + DiffusionPipeline, + ort.InferenceSession, +] +"""Type alias for any kind of runtime, in-memory model representation. For example, a torch module or diffusers pipeline.""" + + +class BaseModelType(str, Enum): + """An enumeration of base model architectures. For example, Stable Diffusion 1.x, Stable Diffusion 2.x, FLUX, etc. + + Every model config must have a base architecture type. + + Not all models are associated with a base architecture. For example, CLIP models are their own thing, not related + to any particular model architecture. To simplify internal APIs and make it easier to work with models, we use a + fallback/null value `BaseModelType.Any` for these models, instead of making the model base optional.""" + + Any = "any" + """`Any` is essentially a fallback/null value for models with no base architecture association. + For example, CLIP models are not related to Stable Diffusion, FLUX, or any other model arch.""" + StableDiffusion1 = "sd-1" + """Indicates the model is associated with the Stable Diffusion 1.x model architecture, including 1.4 and 1.5.""" + StableDiffusion2 = "sd-2" + """Indicates the model is associated with the Stable Diffusion 2.x model architecture, including 2.0 and 2.1.""" + StableDiffusion3 = "sd-3" + """Indicates the model is associated with the Stable Diffusion 3.5 model architecture.""" + StableDiffusionXL = "sdxl" + """Indicates the model is associated with the Stable Diffusion XL model architecture.""" + StableDiffusionXLRefiner = "sdxl-refiner" + """Indicates the model is associated with the Stable Diffusion XL Refiner model architecture.""" + Flux = "flux" + """Indicates the model is associated with FLUX.1 model architecture, including FLUX Dev, Schnell and Fill.""" + Flux2 = "flux2" + """Indicates the model is associated with FLUX.2 model architecture, including FLUX2 Klein.""" + CogView4 = "cogview4" + """Indicates the model is associated with CogView 4 model architecture.""" + ZImage = "z-image" + """Indicates the model is associated with Z-Image model architecture, including Z-Image-Turbo.""" + External = "external" + """Indicates the model is hosted by an external provider.""" + QwenImage = "qwen-image" + """Indicates the model is associated with Qwen Image Edit 2511 model architecture.""" + Anima = "anima" + """Indicates the model is associated with Anima model architecture (Cosmos Predict2 DiT + LLM Adapter).""" + Unknown = "unknown" + """Indicates the model's base architecture is unknown.""" + + +class ModelType(str, Enum): + """Model type.""" + + ONNX = "onnx" + Main = "main" + VAE = "vae" + LoRA = "lora" + ControlLoRa = "control_lora" + ControlNet = "controlnet" # used by model_probe + TextualInversion = "embedding" + IPAdapter = "ip_adapter" + CLIPVision = "clip_vision" + CLIPEmbed = "clip_embed" + T2IAdapter = "t2i_adapter" + T5Encoder = "t5_encoder" + Qwen3Encoder = "qwen3_encoder" + QwenVLEncoder = "qwen_vl_encoder" + SpandrelImageToImage = "spandrel_image_to_image" + SigLIP = "siglip" + FluxRedux = "flux_redux" + LlavaOnevision = "llava_onevision" + TextLLM = "text_llm" + ExternalImageGenerator = "external_image_generator" + Unknown = "unknown" + + +class SubModelType(str, Enum): + """Submodel type.""" + + UNet = "unet" + Transformer = "transformer" + TextEncoder = "text_encoder" + TextEncoder2 = "text_encoder_2" + TextEncoder3 = "text_encoder_3" + Tokenizer = "tokenizer" + Tokenizer2 = "tokenizer_2" + Tokenizer3 = "tokenizer_3" + VAE = "vae" + VAEDecoder = "vae_decoder" + VAEEncoder = "vae_encoder" + Scheduler = "scheduler" + SafetyChecker = "safety_checker" + + +class ClipVariantType(str, Enum): + """Variant type.""" + + L = "large" + G = "gigantic" + + +class ModelVariantType(str, Enum): + """Variant type.""" + + Normal = "normal" + Inpaint = "inpaint" + Depth = "depth" + + +class FluxVariantType(str, Enum): + """FLUX.1 model variants.""" + + Schnell = "schnell" + Dev = "dev" + DevFill = "dev_fill" + + +class Flux2VariantType(str, Enum): + """FLUX.2 model variants.""" + + Klein4B = "klein_4b" + """Flux2 Klein 4B variant using Qwen3 4B text encoder (distilled).""" + + Klein4BBase = "klein_4b_base" + """Flux2 Klein 4B Base variant - undistilled foundation model using Qwen3 4B text encoder.""" + + Klein9B = "klein_9b" + """Flux2 Klein 9B variant using Qwen3 8B text encoder (distilled).""" + + Klein9BBase = "klein_9b_base" + """Flux2 Klein 9B Base variant - undistilled foundation model using Qwen3 8B text encoder.""" + + +class ZImageVariantType(str, Enum): + """Z-Image model variants.""" + + Turbo = "turbo" + """Z-Image Turbo - distilled model optimized for 8 steps, no CFG support.""" + + ZBase = "zbase" + """Z-Image Base - undistilled foundation model with full CFG and negative prompt support.""" + + +class QwenImageVariantType(str, Enum): + """Qwen Image model variants.""" + + Generate = "generate" + """Qwen Image - text-to-image generation model.""" + + Edit = "edit" + """Qwen Image Edit - image editing model with reference image support.""" + + +class Qwen3VariantType(str, Enum): + """Qwen3 text encoder variants based on model size.""" + + Qwen3_4B = "qwen3_4b" + """Qwen3 4B text encoder (hidden_size=2560). Used by FLUX.2 Klein 4B and Z-Image.""" + + Qwen3_8B = "qwen3_8b" + """Qwen3 8B text encoder (hidden_size=4096). Used by FLUX.2 Klein 9B.""" + + Qwen3_06B = "qwen3_06b" + """Qwen3 0.6B text encoder (hidden_size=1024). Used by Anima.""" + + +class ModelFormat(str, Enum): + """Storage format of model.""" + + OMI = "omi" + Diffusers = "diffusers" + Checkpoint = "checkpoint" + LyCORIS = "lycoris" + ONNX = "onnx" + Olive = "olive" + EmbeddingFile = "embedding_file" + EmbeddingFolder = "embedding_folder" + InvokeAI = "invokeai" + T5Encoder = "t5_encoder" + Qwen3Encoder = "qwen3_encoder" + QwenVLEncoder = "qwen_vl_encoder" + BnbQuantizedLlmInt8b = "bnb_quantized_int8b" + BnbQuantizednf4b = "bnb_quantized_nf4b" + GGUFQuantized = "gguf_quantized" + ExternalApi = "external_api" + Unknown = "unknown" + + +class SchedulerPredictionType(str, Enum): + """Scheduler prediction type.""" + + Epsilon = "epsilon" + VPrediction = "v_prediction" + Sample = "sample" + + +class ModelRepoVariant(str, Enum): + """Various hugging face variants on the diffusers format.""" + + Default = "" # model files without "fp16" or other qualifier + FP16 = "fp16" + FP32 = "fp32" + ONNX = "onnx" + OpenVINO = "openvino" + Flax = "flax" + + +class ModelSourceType(str, Enum): + """Model source type.""" + + Path = "path" + Url = "url" + HFRepoID = "hf_repo_id" + External = "external" + + +class FluxLoRAFormat(str, Enum): + """Flux LoRA formats.""" + + Diffusers = "flux.diffusers" + Kohya = "flux.kohya" + OneTrainer = "flux.onetrainer" + Control = "flux.control" + AIToolkit = "flux.aitoolkit" + XLabs = "flux.xlabs" + BflPeft = "flux.bfl_peft" + OneTrainerBfl = "flux.onetrainer_bfl" + + +AnyVariant: TypeAlias = Union[ + ModelVariantType, + ClipVariantType, + FluxVariantType, + Flux2VariantType, + ZImageVariantType, + QwenImageVariantType, + Qwen3VariantType, +] +variant_type_adapter = TypeAdapter[ + ModelVariantType + | ClipVariantType + | FluxVariantType + | Flux2VariantType + | ZImageVariantType + | QwenImageVariantType + | Qwen3VariantType +]( + ModelVariantType + | ClipVariantType + | FluxVariantType + | Flux2VariantType + | ZImageVariantType + | QwenImageVariantType + | Qwen3VariantType +) diff --git a/invokeai/backend/model_manager/util/libc_util.py b/invokeai/backend/model_manager/util/libc_util.py new file mode 100644 index 00000000000..8d104093085 --- /dev/null +++ b/invokeai/backend/model_manager/util/libc_util.py @@ -0,0 +1,78 @@ +import ctypes + + +class Struct_mallinfo2(ctypes.Structure): + """A ctypes Structure that matches the libc mallinfo2 struct. + + Docs: + - https://man7.org/linux/man-pages/man3/mallinfo.3.html + - https://www.gnu.org/software/libc/manual/html_node/Statistics-of-Malloc.html + + struct mallinfo2 { + size_t arena; /* Non-mmapped space allocated (bytes) */ + size_t ordblks; /* Number of free chunks */ + size_t smblks; /* Number of free fastbin blocks */ + size_t hblks; /* Number of mmapped regions */ + size_t hblkhd; /* Space allocated in mmapped regions (bytes) */ + size_t usmblks; /* See below */ + size_t fsmblks; /* Space in freed fastbin blocks (bytes) */ + size_t uordblks; /* Total allocated space (bytes) */ + size_t fordblks; /* Total free space (bytes) */ + size_t keepcost; /* Top-most, releasable space (bytes) */ + }; + """ + + _fields_ = [ + ("arena", ctypes.c_size_t), + ("ordblks", ctypes.c_size_t), + ("smblks", ctypes.c_size_t), + ("hblks", ctypes.c_size_t), + ("hblkhd", ctypes.c_size_t), + ("usmblks", ctypes.c_size_t), + ("fsmblks", ctypes.c_size_t), + ("uordblks", ctypes.c_size_t), + ("fordblks", ctypes.c_size_t), + ("keepcost", ctypes.c_size_t), + ] + + def __str__(self) -> str: + s = "" + s += ( + f"{'arena': <10}= {(self.arena / 2**30):15.5f} # Non-mmapped space allocated (GB) (uordblks + fordblks)\n" + ) + s += f"{'ordblks': <10}= {(self.ordblks): >15} # Number of free chunks\n" + s += f"{'smblks': <10}= {(self.smblks): >15} # Number of free fastbin blocks \n" + s += f"{'hblks': <10}= {(self.hblks): >15} # Number of mmapped regions \n" + s += f"{'hblkhd': <10}= {(self.hblkhd / 2**30):15.5f} # Space allocated in mmapped regions (GB)\n" + s += f"{'usmblks': <10}= {(self.usmblks): >15} # Unused\n" + s += f"{'fsmblks': <10}= {(self.fsmblks / 2**30):15.5f} # Space in freed fastbin blocks (GB)\n" + s += ( + f"{'uordblks': <10}= {(self.uordblks / 2**30):15.5f} # Space used by in-use allocations (non-mmapped)" + " (GB)\n" + ) + s += f"{'fordblks': <10}= {(self.fordblks / 2**30):15.5f} # Space in free blocks (non-mmapped) (GB)\n" + s += f"{'keepcost': <10}= {(self.keepcost / 2**30):15.5f} # Top-most, releasable space (GB)\n" + return s + + +class LibcUtil: + """A utility class for interacting with the C Standard Library (`libc`) via ctypes. + + Note that this class will raise on __init__() if 'libc.so.6' can't be found. Take care to handle environments where + this shared library is not available. + + TODO: Improve cross-OS compatibility of this class. + """ + + def __init__(self) -> None: + self._libc = ctypes.cdll.LoadLibrary("libc.so.6") + + def mallinfo2(self) -> Struct_mallinfo2: + """Calls `libc` `mallinfo2`. + + Docs: https://man7.org/linux/man-pages/man3/mallinfo.3.html + """ + mallinfo2 = self._libc.mallinfo2 + mallinfo2.restype = Struct_mallinfo2 + result: Struct_mallinfo2 = mallinfo2() + return result diff --git a/invokeai/backend/model_manager/util/lora_metadata_extractor.py b/invokeai/backend/model_manager/util/lora_metadata_extractor.py new file mode 100644 index 00000000000..12b10739354 --- /dev/null +++ b/invokeai/backend/model_manager/util/lora_metadata_extractor.py @@ -0,0 +1,146 @@ +"""Utility functions for extracting metadata from LoRA model files.""" + +import json +import logging +from pathlib import Path +from typing import Any, Dict, Optional, Set, Tuple + +from PIL import Image + +from invokeai.app.util.thumbnails import make_thumbnail +from invokeai.backend.model_manager.configs.factory import AnyModelConfig +from invokeai.backend.model_manager.taxonomy import ModelType + +logger = logging.getLogger(__name__) + + +def extract_lora_metadata( + model_path: Path, model_key: str, model_images_path: Path +) -> Tuple[Optional[str], Optional[Set[str]]]: + """ + Extract metadata for a LoRA model from associated JSON and image files. + + Args: + model_path: Path to the LoRA model file + model_key: Unique key for the model + model_images_path: Path to the model images directory + + Returns: + Tuple of (description, trigger_phrases) + """ + model_stem = model_path.stem + model_dir = model_path.parent + + # Find and process preview image + _process_preview_image(model_stem, model_dir, model_key, model_images_path) + + # Extract metadata from JSON + description, trigger_phrases = _extract_json_metadata(model_stem, model_dir) + + return description, trigger_phrases + + +def _process_preview_image(model_stem: str, model_dir: Path, model_key: str, model_images_path: Path) -> bool: + """Find and process a preview image for the model, saving it to the model images store.""" + image_extensions = [".png", ".jpg", ".jpeg", ".webp"] + + for ext in image_extensions: + image_path = model_dir / f"{model_stem}{ext}" + if image_path.exists(): + try: + # Open the image + with Image.open(image_path) as img: + # Create thumbnail and save to model images directory + thumbnail = make_thumbnail(img, 256) + thumbnail_path = model_images_path / f"{model_key}.webp" + thumbnail.save(thumbnail_path, format="webp") + + logger.info(f"Processed preview image {image_path.name} for model {model_key}") + return True + + except Exception as e: + logger.warning(f"Failed to process preview image {image_path.name}: {e}") + return False + + return False + + +def _extract_json_metadata(model_stem: str, model_dir: Path) -> Tuple[Optional[str], Optional[Set[str]]]: + """Extract metadata from a JSON file with the same name as the model.""" + json_path = model_dir / f"{model_stem}.json" + + if not json_path.exists(): + return None, None + + try: + with open(json_path, "r", encoding="utf-8") as f: + metadata = json.load(f) + + # Extract description + description = _build_description(metadata) + + # Extract trigger phrases + trigger_phrases = _extract_trigger_phrases(metadata) + + if description or trigger_phrases: + logger.info(f"Applied metadata from {json_path.name}") + + return description, trigger_phrases + + except (json.JSONDecodeError, IOError, Exception) as e: + logger.warning(f"Failed to read metadata from {json_path}: {e}") + return None, None + + +def _build_description(metadata: Dict[str, Any]) -> Optional[str]: + """Build a description from metadata fields.""" + description_parts = [] + + if description := metadata.get("description"): + description_parts.append(str(description).strip()) + + if notes := metadata.get("notes"): + description_parts.append(str(notes).strip()) + + return " | ".join(description_parts) if description_parts else None + + +def _extract_trigger_phrases(metadata: Dict[str, Any]) -> Optional[Set[str]]: + """Extract trigger phrases from metadata.""" + if not (activation_text := metadata.get("activation text")): + return None + + activation_text = str(activation_text).strip() + if not activation_text: + return None + + # Split on commas and clean up each phrase + phrases = [phrase.strip() for phrase in activation_text.split(",") if phrase.strip()] + + return set(phrases) if phrases else None + + +def apply_lora_metadata(info: AnyModelConfig, model_path: Path, model_images_path: Path) -> None: + """ + Apply extracted metadata to a LoRA model configuration. + + Args: + info: The model configuration to update + model_path: Path to the LoRA model file + model_images_path: Path to the model images directory + """ + # Only process LoRA models + if info.type != ModelType.LoRA: + return + + # Extract and apply metadata + description, trigger_phrases = extract_lora_metadata(model_path, info.key, model_images_path) + + # We don't set cover_image path in the config anymore since images are stored + # separately in the model images store by model key + + if description: + info.description = description + + if trigger_phrases: + info.trigger_phrases = trigger_phrases diff --git a/invokeai/backend/model_manager/util/model_util.py b/invokeai/backend/model_manager/util/model_util.py new file mode 100644 index 00000000000..c153129353b --- /dev/null +++ b/invokeai/backend/model_manager/util/model_util.py @@ -0,0 +1,210 @@ +"""Utilities for parsing model files, used mostly by legacy_probe.py""" + +import json +from pathlib import Path +from typing import Dict, Optional, Union + +import picklescan.scanner as pscan +import safetensors +import torch + +from invokeai.app.services.config.config_default import get_config +from invokeai.backend.model_manager.taxonomy import ClipVariantType +from invokeai.backend.quantization.gguf.loaders import gguf_sd_loader +from invokeai.backend.util.logging import InvokeAILogger + +logger = InvokeAILogger.get_logger() + + +def _fast_safetensors_reader(path: str) -> Dict[str, torch.Tensor]: + checkpoint = {} + device = torch.device("meta") + with open(path, "rb") as f: + definition_len = int.from_bytes(f.read(8), "little") + definition_json = f.read(definition_len) + definition = json.loads(definition_json) + + if "__metadata__" in definition and definition["__metadata__"].get("format", "pt") not in { + "pt", + "torch", + "pytorch", + }: + raise Exception("Supported only pytorch safetensors files") + definition.pop("__metadata__", None) + + for key, info in definition.items(): + dtype = { + "I8": torch.int8, + "I16": torch.int16, + "I32": torch.int32, + "I64": torch.int64, + "F16": torch.float16, + "F32": torch.float32, + "F64": torch.float64, + }[info["dtype"]] + + checkpoint[key] = torch.empty(info["shape"], dtype=dtype, device=device) + + return checkpoint + + +def read_checkpoint_meta(path: Union[str, Path], scan: bool = True) -> Dict[str, torch.Tensor]: + if str(path).endswith(".safetensors"): + try: + path_str = path.as_posix() if isinstance(path, Path) else path + checkpoint = _fast_safetensors_reader(path_str) + except Exception: + # TODO: create issue for support "meta"? + checkpoint = safetensors.torch.load_file(path, device="cpu") + elif str(path).endswith(".gguf"): + # The GGUF reader used here uses numpy memmap, so these tensors are not loaded into memory during this function + checkpoint = gguf_sd_loader(Path(path), compute_dtype=torch.float32) + else: + if scan: + scan_result = pscan.scan_file_path(path) + if scan_result.infected_files != 0: + if get_config().unsafe_disable_picklescan: + logger.warning( + f"The model {path} is potentially infected by malware, but picklescan is disabled. " + "Proceeding with caution." + ) + else: + raise RuntimeError(f"The model {path} is potentially infected by malware. Aborting import.") + if scan_result.scan_err: + if get_config().unsafe_disable_picklescan: + logger.warning( + f"Error scanning the model at {path} for malware, but picklescan is disabled. " + "Proceeding with caution." + ) + else: + raise RuntimeError(f"Error scanning the model at {path} for malware. Aborting import.") + + checkpoint = torch.load(path, map_location=torch.device("meta")) + return checkpoint + + +def lora_token_vector_length(checkpoint: dict[str | int, torch.Tensor]) -> Optional[int]: + """ + Given a checkpoint in memory, return the lora token vector length + + :param checkpoint: The checkpoint + """ + + def _get_shape_1(key: str, tensor: torch.Tensor, checkpoint: dict[str | int, torch.Tensor]) -> Optional[int]: + lora_token_vector_length = None + + if "." not in key: + return lora_token_vector_length # wrong key format + model_key, lora_key = key.split(".", 1) + + # check lora/locon + if lora_key == "lora_down.weight": + lora_token_vector_length = tensor.shape[1] + + # check loha (don't worry about hada_t1/hada_t2 as it used only in 4d shapes) + elif lora_key in ["hada_w1_b", "hada_w2_b"]: + lora_token_vector_length = tensor.shape[1] + + # check lokr (don't worry about lokr_t2 as it used only in 4d shapes) + elif "lokr_" in lora_key: + if model_key + ".lokr_w1" in checkpoint: + _lokr_w1 = checkpoint[model_key + ".lokr_w1"] + elif model_key + "lokr_w1_b" in checkpoint: + _lokr_w1 = checkpoint[model_key + ".lokr_w1_b"] + else: + return lora_token_vector_length # unknown format + + if model_key + ".lokr_w2" in checkpoint: + _lokr_w2 = checkpoint[model_key + ".lokr_w2"] + elif model_key + "lokr_w2_b" in checkpoint: + _lokr_w2 = checkpoint[model_key + ".lokr_w2_b"] + else: + return lora_token_vector_length # unknown format + + lora_token_vector_length = _lokr_w1.shape[1] * _lokr_w2.shape[1] + + elif lora_key == "diff": + lora_token_vector_length = tensor.shape[1] + + # ia3 can be detected only by shape[0] in text encoder + elif lora_key == "weight" and "lora_unet_" not in model_key: + lora_token_vector_length = tensor.shape[0] + + return lora_token_vector_length + + lora_token_vector_length = None + lora_te1_length = None + lora_te2_length = None + for key, tensor in checkpoint.items(): + if isinstance(key, int): + continue + if key.startswith("lora_unet_") and ("_attn2_to_k." in key or "_attn2_to_v." in key): + lora_token_vector_length = _get_shape_1(key, tensor, checkpoint) + elif key.startswith("lora_unet_") and ( + "time_emb_proj.lora_down" in key + ): # recognizes format at https://civitai.com/models/224641 + lora_token_vector_length = _get_shape_1(key, tensor, checkpoint) + elif key.startswith("lora_te") and "_self_attn_" in key: + tmp_length = _get_shape_1(key, tensor, checkpoint) + if key.startswith("lora_te_"): + lora_token_vector_length = tmp_length + elif key.startswith("lora_te1_"): + lora_te1_length = tmp_length + elif key.startswith("lora_te2_"): + lora_te2_length = tmp_length + + if lora_te1_length is not None and lora_te2_length is not None: + lora_token_vector_length = lora_te1_length + lora_te2_length + + if lora_token_vector_length is not None: + break + + return lora_token_vector_length + + +def convert_bundle_to_flux_transformer_checkpoint( + transformer_state_dict: dict[str, torch.Tensor], +) -> dict[str, torch.Tensor]: + original_state_dict: dict[str, torch.Tensor] = {} + keys_to_remove: list[str] = [] + + for k, v in transformer_state_dict.items(): + if not k.startswith("model.diffusion_model"): + keys_to_remove.append(k) # This can be removed in the future if we only want to delete transformer keys + continue + if k.endswith("scale"): + # Scale math must be done at bfloat16 due to our current flux model + # support limitations at inference time + v = v.to(dtype=torch.bfloat16) + new_key = k.replace("model.diffusion_model.", "") + original_state_dict[new_key] = v + keys_to_remove.append(k) + + # Remove processed keys from the original dictionary, leaving others in case + # other model state dicts need to be pulled + for k in keys_to_remove: + del transformer_state_dict[k] + + return original_state_dict + + +def get_clip_variant_type(location: str) -> Optional[ClipVariantType]: + try: + path = Path(location) + config_path = path / "config.json" + if not config_path.exists(): + config_path = path / "text_encoder" / "config.json" + if not config_path.exists(): + return ClipVariantType.L + with open(config_path) as file: + clip_conf = json.load(file) + hidden_size = clip_conf.get("hidden_size", -1) + match hidden_size: + case 1280: + return ClipVariantType.G + case 768: + return ClipVariantType.L + case _: + return ClipVariantType.L + except Exception: + return ClipVariantType.L diff --git a/invokeai/backend/model_manager/util/select_hf_files.py b/invokeai/backend/model_manager/util/select_hf_files.py new file mode 100644 index 00000000000..a8428f4edcd --- /dev/null +++ b/invokeai/backend/model_manager/util/select_hf_files.py @@ -0,0 +1,218 @@ +# Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Development Team +""" +Select the files from a HuggingFace repository needed for a particular model variant. + +Usage: +``` +from invokeai.backend.model_manager.util.select_hf_files import select_hf_model_files +from invokeai.backend.model_manager.metadata.fetch import HuggingFaceMetadataFetch + +metadata = HuggingFaceMetadataFetch().from_url("https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0") +files_to_download = select_hf_model_files(metadata.files, variant='onnx') +``` +""" + +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Optional, Set + +from invokeai.backend.model_manager.taxonomy import ModelRepoVariant + + +def filter_files( + files: List[Path], + variant: Optional[ModelRepoVariant] = None, + subfolder: Optional[Path] = None, + subfolders: Optional[List[Path]] = None, +) -> List[Path]: + """ + Take a list of files in a HuggingFace repo root and return paths to files needed to load the model. + + :param files: List of files relative to the repo root. + :param subfolder: Filter by the indicated subfolder (deprecated, use subfolders instead). + :param subfolders: Filter by multiple subfolders. Files from any of these subfolders will be included. + :param variant: Filter by files belonging to a particular variant, such as fp16. + + The file list can be obtained from the `files` field of HuggingFaceMetadata, + as defined in `invokeai.backend.model_manager.metadata.metadata_base`. + """ + variant = variant or ModelRepoVariant.Default + paths: List[Path] = [] + + if not files: + return [] + + root = files[0].parts[0] if files[0].parts else Path(".") + + # Build list of subfolders to filter by + filter_subfolders: List[Path] = [] + if subfolders: + filter_subfolders = subfolders + elif subfolder: + filter_subfolders = [subfolder] + + # if the subfolder is a single file, then bypass the selection and just return it + if len(filter_subfolders) == 1: + sf = filter_subfolders[0] + if sf.suffix in [".safetensors", ".bin", ".onnx", ".xml", ".pth", ".pt", ".ckpt", ".msgpack"]: + return [root / sf] + + # Start by filtering on model file extensions, discarding images, docs, etc + for file in files: + if file.name.endswith((".json", ".txt", ".jinja")): # .jinja for chat templates + paths.append(file) + elif file.name.endswith( + ( + "learned_embeds.bin", + "ip_adapter.bin", + "lora_weights.safetensors", + "weights.pb", + "onnx_data", + "spiece.model", # Added for `black-forest-labs/FLUX.1-schnell`. + ) + ): + paths.append(file) + # BRITTLENESS WARNING!! + # Diffusers models always seem to have "model" in their name, and the regex filter below is applied to avoid + # downloading random checkpoints that might also be in the repo. However there is no guarantee + # that a checkpoint doesn't contain "model" in its name, and no guarantee that future diffusers models + # will adhere to this naming convention, so this is an area to be careful of. + elif re.search(r"model.*\.(safetensors|bin|onnx|xml|pth|pt|ckpt|msgpack)$", file.name): + paths.append(file) + + # limit search to subfolder(s) if requested + if filter_subfolders: + absolute_subfolders = [root / sf for sf in filter_subfolders] + paths = [x for x in paths if any(Path(sf) in x.parents for sf in absolute_subfolders)] + + # _filter_by_variant uniquifies the paths and returns a set + return sorted(_filter_by_variant(paths, variant)) + + +@dataclass +class SubfolderCandidate: + path: Path + score: int + + +def _filter_by_variant(files: List[Path], variant: ModelRepoVariant) -> Set[Path]: + """Select the proper variant files from a list of HuggingFace repo_id paths.""" + result: set[Path] = set() + subfolder_weights: dict[Path, list[SubfolderCandidate]] = {} + safetensors_detected = False + for path in files: + if path.suffix in [".onnx", ".pb", ".onnx_data"]: + if variant == ModelRepoVariant.ONNX: + result.add(path) + + elif "openvino_model" in path.name: + if variant == ModelRepoVariant.OpenVINO: + result.add(path) + + elif "flax_model" in path.name: + if variant == ModelRepoVariant.Flax: + result.add(path) + + # Note: '.model' was added to support: + # https://huggingface.co/black-forest-labs/FLUX.1-schnell/blob/768d12a373ed5cc9ef9a9dea7504dc09fcc14842/tokenizer_2/spiece.model + # Note: '.jinja' was added to support chat templates for FLUX.2 Klein models + elif path.suffix in [".json", ".txt", ".model", ".jinja"]: + result.add(path) + + elif variant in [ + ModelRepoVariant.FP16, + ModelRepoVariant.FP32, + ModelRepoVariant.Default, + ] and path.suffix in [".bin", ".safetensors", ".pt", ".ckpt"]: + # For weights files, we want to select the best one for each subfolder. For example, we may have multiple + # text encoders: + # + # - text_encoder/model.fp16.safetensors + # - text_encoder/model.safetensors + # - text_encoder/pytorch_model.bin + # - text_encoder/pytorch_model.fp16.bin + # + # We prefer safetensors over other file formats and an exact variant match. We'll score each file based on + # variant and format and select the best one. + + if safetensors_detected and path.suffix == ".bin": + continue + + parent = path.parent + score = 0 + + if path.suffix == ".safetensors": + safetensors_detected = True + if parent in subfolder_weights: + subfolder_weights[parent] = [sfc for sfc in subfolder_weights[parent] if sfc.path.suffix != ".bin"] + score += 1 + + candidate_variant_label = path.suffixes[0] if len(path.suffixes) == 2 else None + + # Some special handling is needed here if there is not an exact match and if we cannot infer the variant + # from the file name. In this case, we only give this file a point if the requested variant is FP32 or DEFAULT. + if ( + variant is not ModelRepoVariant.Default + and candidate_variant_label + and candidate_variant_label.startswith(f".{variant.value}") + ) or (not candidate_variant_label and variant in [ModelRepoVariant.FP32, ModelRepoVariant.Default]): + score += 1 + + if parent not in subfolder_weights: + subfolder_weights[parent] = [] + + subfolder_weights[parent].append(SubfolderCandidate(path=path, score=score)) + + else: + continue + + for candidate_list in subfolder_weights.values(): + # Check if at least one of the files has the explicit fp16 variant. + at_least_one_fp16 = False + for candidate in candidate_list: + if len(candidate.path.suffixes) == 2 and candidate.path.suffixes[0].startswith(".fp16"): + at_least_one_fp16 = True + break + + if not at_least_one_fp16: + # If none of the candidates in this candidate_list have the explicit fp16 variant label, then this + # candidate_list probably doesn't adhere to the variant naming convention that we expected. In this case, + # we'll simply keep all the candidates. An example of a model that hits this case is + # `black-forest-labs/FLUX.1-schnell` (as of commit 012d2fd). + for candidate in candidate_list: + result.add(candidate.path) + + # The candidate_list seems to have the expected variant naming convention. We'll select the highest scoring + # candidate. + highest_score_candidate = max(candidate_list, key=lambda candidate: candidate.score) + if highest_score_candidate: + pattern = r"^(.*?)-\d+-of-\d+(\.\w+)$" + match = re.match(pattern, highest_score_candidate.path.as_posix()) + if match: + for candidate in candidate_list: + if candidate.path.as_posix().startswith(match.group(1)) and candidate.path.as_posix().endswith( + match.group(2) + ): + result.add(candidate.path) + else: + result.add(highest_score_candidate.path) + + # If one of the architecture-related variants was specified and no files matched other than + # config and text files then we return an empty list + if ( + variant + and variant in [ModelRepoVariant.ONNX, ModelRepoVariant.OpenVINO, ModelRepoVariant.Flax] + and not any(variant.value in x.name for x in result) + ): + return set() + + # Prune folders that contain just a `config.json`. This happens when + # the requested variant (e.g. "onnx") is missing + directories: Dict[Path, int] = {} + for x in result: + if not x.parent: + continue + directories[x.parent] = directories.get(x.parent, 0) + 1 + + return {x for x in result if directories[x.parent] > 1 or x.name != "config.json"} diff --git a/invokeai/backend/model_patcher.py b/invokeai/backend/model_patcher.py new file mode 100644 index 00000000000..04f99495609 --- /dev/null +++ b/invokeai/backend/model_patcher.py @@ -0,0 +1,180 @@ +# Copyright (c) 2024 Ryan Dick, Lincoln D. Stein, and the InvokeAI Development Team +"""These classes implement model patching with LoRAs and Textual Inversions.""" + +from __future__ import annotations + +import pickle +from contextlib import contextmanager +from typing import Any, Generator, Iterator, List, Optional, Tuple, Type, Union + +import torch +from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel +from transformers import CLIPTextModel, CLIPTextModelWithProjection, CLIPTokenizer + +from invokeai.app.shared.models import FreeUConfig +from invokeai.backend.model_manager.load.optimizations import skip_torch_weight_init +from invokeai.backend.textual_inversion import TextualInversionManager, TextualInversionModelRaw +from invokeai.backend.util.devices import TorchDevice + + +class ModelPatcher: + @staticmethod + @contextmanager + def patch_unet_attention_processor(unet: UNet2DConditionModel, processor_cls: Type[Any]): + """A context manager that patches `unet` with the provided attention processor. + + Args: + unet (UNet2DConditionModel): The UNet model to patch. + processor (Type[Any]): Class which will be initialized for each key and passed to set_attn_processor(...). + """ + unet_orig_processors = unet.attn_processors + + # create separate instance for each attention, to be able modify each attention separately + unet_new_processors = {key: processor_cls() for key in unet_orig_processors.keys()} + try: + unet.set_attn_processor(unet_new_processors) + yield None + + finally: + unet.set_attn_processor(unet_orig_processors) + + @classmethod + @contextmanager + def apply_ti( + cls, + tokenizer: CLIPTokenizer, + text_encoder: Union[CLIPTextModel, CLIPTextModelWithProjection], + ti_list: List[Tuple[str, TextualInversionModelRaw]], + ) -> Iterator[Tuple[CLIPTokenizer, TextualInversionManager]]: + if len(ti_list) == 0: + yield tokenizer, TextualInversionManager(tokenizer) + return + + init_tokens_count = None + new_tokens_added = None + + # TODO: This is required since Transformers 4.32 see + # https://github.com/huggingface/transformers/pull/25088 + # More information by NVIDIA: + # https://docs.nvidia.com/deeplearning/performance/dl-performance-matrix-multiplication/index.html#requirements-tc + # This value might need to be changed in the future and take the GPUs model into account as there seem + # to be ideal values for different GPUS. This value is temporary! + # For references to the current discussion please see https://github.com/invoke-ai/InvokeAI/pull/4817 + pad_to_multiple_of = 8 + + try: + # HACK: The CLIPTokenizer API does not include a way to remove tokens after calling add_tokens(...). As a + # workaround, we create a full copy of `tokenizer` so that its original behavior can be restored after + # exiting this `apply_ti(...)` context manager. + # + # In a previous implementation, the deep copy was obtained with `ti_tokenizer = copy.deepcopy(tokenizer)`, + # but a pickle roundtrip was found to be much faster (1 sec vs. 0.05 secs). + ti_tokenizer = pickle.loads(pickle.dumps(tokenizer)) + ti_manager = TextualInversionManager(ti_tokenizer) + init_tokens_count = text_encoder.resize_token_embeddings(None, pad_to_multiple_of).num_embeddings + + def _get_trigger(ti_name: str, index: int) -> str: + trigger = ti_name + if index > 0: + trigger += f"-!pad-{i}" + return f"<{trigger}>" + + def _get_ti_embedding(model_embeddings: torch.nn.Module, ti: TextualInversionModelRaw) -> torch.Tensor: + # for SDXL models, select the embedding that matches the text encoder's dimensions + if ti.embedding_2 is not None: + return ( + ti.embedding_2 + if ti.embedding_2.shape[1] == model_embeddings.weight.data[0].shape[0] + else ti.embedding + ) + else: + return ti.embedding + + # modify tokenizer + new_tokens_added = 0 + for ti_name, ti in ti_list: + ti_embedding = _get_ti_embedding(text_encoder.get_input_embeddings(), ti) + + for i in range(ti_embedding.shape[0]): + new_tokens_added += ti_tokenizer.add_tokens(_get_trigger(ti_name, i)) + + # Modify text_encoder. + # resize_token_embeddings(...) constructs a new torch.nn.Embedding internally. Initializing the weights of + # this embedding is slow and unnecessary, so we wrap this step in skip_torch_weight_init() to save some + # time. + with skip_torch_weight_init(): + text_encoder.resize_token_embeddings(init_tokens_count + new_tokens_added, pad_to_multiple_of) + model_embeddings = text_encoder.get_input_embeddings() + + for ti_name, ti in ti_list: + assert isinstance(ti, TextualInversionModelRaw) + ti_embedding = _get_ti_embedding(text_encoder.get_input_embeddings(), ti) + + ti_tokens = [] + for i in range(ti_embedding.shape[0]): + embedding = ti_embedding[i] + trigger = _get_trigger(ti_name, i) + + token_id = ti_tokenizer.convert_tokens_to_ids(trigger) + if token_id == ti_tokenizer.unk_token_id: + raise RuntimeError(f"Unable to find token id for token '{trigger}'") + + if model_embeddings.weight.data[token_id].shape != embedding.shape: + raise ValueError( + f"Cannot load embedding for {trigger}. It was trained on a model with token dimension" + f" {embedding.shape[0]}, but the current model has token dimension" + f" {model_embeddings.weight.data[token_id].shape[0]}." + ) + + model_embeddings.weight.data[token_id] = embedding.to( + device=TorchDevice.choose_torch_device(), dtype=text_encoder.dtype + ) + ti_tokens.append(token_id) + + if len(ti_tokens) > 1: + ti_manager.pad_tokens[ti_tokens[0]] = ti_tokens[1:] + + yield ti_tokenizer, ti_manager + + finally: + if init_tokens_count and new_tokens_added: + text_encoder.resize_token_embeddings(init_tokens_count, pad_to_multiple_of) + + @classmethod + @contextmanager + def apply_clip_skip( + cls, + text_encoder: Union[CLIPTextModel, CLIPTextModelWithProjection], + clip_skip: int, + ) -> Generator[None, Any, Any]: + skipped_layers = [] + try: + for _i in range(clip_skip): + skipped_layers.append(text_encoder.text_model.encoder.layers.pop(-1)) + + yield + + finally: + while len(skipped_layers) > 0: + text_encoder.text_model.encoder.layers.append(skipped_layers.pop()) + + @classmethod + @contextmanager + def apply_freeu( + cls, + unet: UNet2DConditionModel, + freeu_config: Optional[FreeUConfig] = None, + ) -> Generator[None, Any, Any]: + did_apply_freeu = False + try: + assert hasattr(unet, "enable_freeu") # mypy doesn't pick up this attribute? + if freeu_config is not None: + unet.enable_freeu(b1=freeu_config.b1, b2=freeu_config.b2, s1=freeu_config.s1, s2=freeu_config.s2) + did_apply_freeu = True + + yield + + finally: + assert hasattr(unet, "disable_freeu") # mypy doesn't pick up this attribute? + if did_apply_freeu: + unet.disable_freeu() diff --git a/invokeai/backend/onnx/onnx_runtime.py b/invokeai/backend/onnx/onnx_runtime.py new file mode 100644 index 00000000000..a8132d4b233 --- /dev/null +++ b/invokeai/backend/onnx/onnx_runtime.py @@ -0,0 +1,223 @@ +# Copyright (c) 2024 The InvokeAI Development Team +import os +import sys +from pathlib import Path +from typing import Any, List, Optional, Tuple, Union + +import numpy as np +import onnx +import torch +from onnx import numpy_helper +from onnxruntime import InferenceSession, SessionOptions, get_available_providers + +from invokeai.backend.raw_model import RawModel + +ONNX_WEIGHTS_NAME = "model.onnx" + + +# NOTE FROM LS: This was copied from Stalker's original implementation. +# I have not yet gone through and fixed all the type hints +class IAIOnnxRuntimeModel(RawModel): + class _tensor_access: + def __init__(self, model): # type: ignore + self.model = model + self.indexes = {} + for idx, obj in enumerate(self.model.proto.graph.initializer): + self.indexes[obj.name] = idx + + def __getitem__(self, key: str): # type: ignore + value = self.model.proto.graph.initializer[self.indexes[key]] + return numpy_helper.to_array(value) + + def __setitem__(self, key: str, value: np.ndarray): # type: ignore + new_node = numpy_helper.from_array(value) + # set_external_data(new_node, location="in-memory-location") + new_node.name = key + # new_node.ClearField("raw_data") + del self.model.proto.graph.initializer[self.indexes[key]] + self.model.proto.graph.initializer.insert(self.indexes[key], new_node) + # self.model.data[key] = OrtValue.ortvalue_from_numpy(value) + + # __delitem__ + + def __contains__(self, key: str) -> bool: + return self.indexes[key] in self.model.proto.graph.initializer + + def items(self) -> List[Tuple[str, Any]]: # fixme + raise NotImplementedError("tensor.items") + # return [(obj.name, obj) for obj in self.raw_proto] + + def keys(self) -> List[str]: + return list(self.indexes.keys()) + + def values(self) -> List[Any]: # fixme + raise NotImplementedError("tensor.values") + # return [obj for obj in self.raw_proto] + + def size(self) -> int: + bytesSum = 0 + for node in self.model.proto.graph.initializer: + bytesSum += sys.getsizeof(node.raw_data) + return bytesSum + + class _access_helper: + def __init__(self, raw_proto): # type: ignore + self.indexes = {} + self.raw_proto = raw_proto + for idx, obj in enumerate(raw_proto): + self.indexes[obj.name] = idx + + def __getitem__(self, key: str): # type: ignore + return self.raw_proto[self.indexes[key]] + + def __setitem__(self, key: str, value): # type: ignore + index = self.indexes[key] + del self.raw_proto[index] + self.raw_proto.insert(index, value) + + # __delitem__ + + def __contains__(self, key: str) -> bool: + return key in self.indexes + + def items(self) -> List[Tuple[str, Any]]: + return [(obj.name, obj) for obj in self.raw_proto] + + def keys(self) -> List[str]: + return list(self.indexes.keys()) + + def values(self) -> List[Any]: # fixme + return list(self.raw_proto) + + def __init__(self, model_path: str, provider: Optional[str]): + self.path = model_path + self.session = None + self.provider = provider + """ + self.data_path = self.path + "_data" + if not os.path.exists(self.data_path): + print(f"Moving model tensors to separate file: {self.data_path}") + tmp_proto = onnx.load(model_path, load_external_data=True) + onnx.save_model(tmp_proto, self.path, save_as_external_data=True, all_tensors_to_one_file=True, location=os.path.basename(self.data_path), size_threshold=1024, convert_attribute=False) + del tmp_proto + gc.collect() + + self.proto = onnx.load(model_path, load_external_data=False) + """ + + self.proto = onnx.load(model_path, load_external_data=True) + # self.data = dict() + # for tensor in self.proto.graph.initializer: + # name = tensor.name + + # if tensor.HasField("raw_data"): + # npt = numpy_helper.to_array(tensor) + # orv = OrtValue.ortvalue_from_numpy(npt) + # # self.data[name] = orv + # # set_external_data(tensor, location="in-memory-location") + # tensor.name = name + # # tensor.ClearField("raw_data") + + self.nodes = self._access_helper(self.proto.graph.node) # type: ignore + # self.initializers = self._access_helper(self.proto.graph.initializer) + # print(self.proto.graph.input) + # print(self.proto.graph.initializer) + + self.tensors = self._tensor_access(self) # type: ignore + + # TODO: integrate with model manager/cache + def create_session(self, height=None, width=None): + if self.session is None or self.session_width != width or self.session_height != height: + # onnx.save(self.proto, "tmp.onnx") + # onnx.save_model(self.proto, "tmp.onnx", save_as_external_data=True, all_tensors_to_one_file=True, location="tmp.onnx_data", size_threshold=1024, convert_attribute=False) + # TODO: something to be able to get weight when they already moved outside of model proto + # (trimmed_model, external_data) = buffer_external_data_tensors(self.proto) + sess = SessionOptions() + # self._external_data.update(**external_data) + # sess.add_external_initializers(list(self.data.keys()), list(self.data.values())) + # sess.enable_profiling = True + + # sess.intra_op_num_threads = 1 + # sess.inter_op_num_threads = 1 + # sess.execution_mode = ExecutionMode.ORT_SEQUENTIAL + # sess.graph_optimization_level = GraphOptimizationLevel.ORT_ENABLE_ALL + # sess.enable_cpu_mem_arena = True + # sess.enable_mem_pattern = True + # sess.add_session_config_entry("session.intra_op.use_xnnpack_threadpool", "1") ########### It's the key code + self.session_height = height + self.session_width = width + if height and width: + sess.add_free_dimension_override_by_name("unet_sample_batch", 2) + sess.add_free_dimension_override_by_name("unet_sample_channels", 4) + sess.add_free_dimension_override_by_name("unet_hidden_batch", 2) + sess.add_free_dimension_override_by_name("unet_hidden_sequence", 77) + sess.add_free_dimension_override_by_name("unet_sample_height", self.session_height) + sess.add_free_dimension_override_by_name("unet_sample_width", self.session_width) + sess.add_free_dimension_override_by_name("unet_time_batch", 1) + providers = [] + if self.provider: + providers.append(self.provider) + else: + providers = get_available_providers() + if "TensorrtExecutionProvider" in providers: + providers.remove("TensorrtExecutionProvider") + try: + self.session = InferenceSession(self.proto.SerializeToString(), providers=providers, sess_options=sess) + except Exception as e: + raise e + # self.session = InferenceSession("tmp.onnx", providers=[self.provider], sess_options=self.sess_options) + # self.io_binding = self.session.io_binding() + + def release_session(self): + self.session = None + import gc + + gc.collect() + return + + def __call__(self, **kwargs): + if self.session is None: + raise Exception("You should call create_session before running model") + + inputs = {k: np.array(v) for k, v in kwargs.items()} + # output_names = self.session.get_outputs() + # for k in inputs: + # self.io_binding.bind_cpu_input(k, inputs[k]) + # for name in output_names: + # self.io_binding.bind_output(name.name) + # self.session.run_with_iobinding(self.io_binding, None) + # return self.io_binding.copy_outputs_to_cpu() + return self.session.run(None, inputs) + + # compatability with RawModel ABC + def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None: + pass + + # compatability with diffusers load code + @classmethod + def from_pretrained( + cls, + model_id: Union[str, Path], + subfolder: Optional[Union[str, Path]] = None, + file_name: Optional[str] = None, + provider: Optional[str] = None, + sess_options: Optional["SessionOptions"] = None, + **kwargs: Any, + ) -> Any: # fixme + file_name = file_name or ONNX_WEIGHTS_NAME + + if os.path.isdir(model_id): + model_path = model_id + if subfolder is not None: + model_path = os.path.join(model_path, subfolder) + model_path = os.path.join(model_path, file_name) + + else: + model_path = model_id + + # load model from local directory + if not os.path.isfile(model_path): + raise Exception(f"Model not found: {model_path}") + + # TODO: session options + return cls(str(model_path), provider=provider) diff --git a/invokeai/backend/patches/__init__.py b/invokeai/backend/patches/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/patches/layer_patcher.py b/invokeai/backend/patches/layer_patcher.py new file mode 100644 index 00000000000..fbfcd04de20 --- /dev/null +++ b/invokeai/backend/patches/layer_patcher.py @@ -0,0 +1,324 @@ +import re +from contextlib import contextmanager +from typing import Dict, Iterable, Optional, Tuple + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.flux_control_lora_layer import FluxControlLoRALayer +from invokeai.backend.patches.model_patch_raw import ModelPatchRaw +from invokeai.backend.patches.pad_with_zeros import pad_with_zeros +from invokeai.backend.util import InvokeAILogger +from invokeai.backend.util.devices import TorchDevice +from invokeai.backend.util.original_weights_storage import OriginalWeightsStorage + + +class LayerPatcher: + @staticmethod + @torch.no_grad() + @contextmanager + def apply_smart_model_patches( + model: torch.nn.Module, + patches: Iterable[Tuple[ModelPatchRaw, float]], + prefix: str, + dtype: torch.dtype, + cached_weights: Optional[Dict[str, torch.Tensor]] = None, + force_direct_patching: bool = False, + force_sidecar_patching: bool = False, + suppress_warning_layers: Optional[re.Pattern] = None, + ): + """Apply 'smart' model patching that chooses whether to use direct patching or a sidecar wrapper for each + module. + """ + + # original_weights are stored for unpatching layers that are directly patched. + original_weights = OriginalWeightsStorage(cached_weights) + # original_modules are stored for unpatching layers that are wrapped. + original_modules: dict[str, torch.nn.Module] = {} + try: + for patch, patch_weight in patches: + LayerPatcher.apply_smart_model_patch( + model=model, + prefix=prefix, + patch=patch, + patch_weight=patch_weight, + original_weights=original_weights, + original_modules=original_modules, + dtype=dtype, + force_direct_patching=force_direct_patching, + force_sidecar_patching=force_sidecar_patching, + suppress_warning_layers=suppress_warning_layers, + ) + + yield + finally: + # Restore directly patched layers. + for param_key, weight in original_weights.get_changed_weights(): + cur_param = model.get_parameter(param_key) + cur_param.data = weight.to(dtype=cur_param.dtype, device=cur_param.device, copy=True) + + # Clear patches from all patched modules. + # Note: This logic assumes no nested modules in original_modules. + for orig_module in original_modules.values(): + orig_module.clear_patches() + + @staticmethod + @torch.no_grad() + def apply_smart_model_patch( + model: torch.nn.Module, + prefix: str, + patch: ModelPatchRaw, + patch_weight: float, + original_weights: OriginalWeightsStorage, + original_modules: dict[str, torch.nn.Module], + dtype: torch.dtype, + force_direct_patching: bool, + force_sidecar_patching: bool, + suppress_warning_layers: Optional[re.Pattern] = None, + ): + """Apply a single LoRA patch to a model using the 'smart' patching strategy that chooses whether to use direct + patching or a sidecar wrapper for each module. + """ + if patch_weight == 0: + return + + # If the layer keys contain a dot, then they are not flattened, and can be directly used to access model + # submodules. If the layer keys do not contain a dot, then they are flattened, meaning that all '.' have been + # replaced with '_'. Non-flattened keys are preferred, because they allow submodules to be accessed directly + # without searching, but some legacy code still uses flattened keys. + first_key = next(iter(patch.layers.keys())) + layer_keys_are_flattened = "." not in first_key + + prefix_len = len(prefix) + + for layer_key, layer in patch.layers.items(): + if not layer_key.startswith(prefix): + continue + + try: + module_key, module = LayerPatcher._get_submodule( + model, layer_key[prefix_len:], layer_key_is_flattened=layer_keys_are_flattened + ) + except AttributeError: + if suppress_warning_layers and suppress_warning_layers.search(layer_key): + pass + else: + logger = InvokeAILogger.get_logger(LayerPatcher.__name__) + logger.warning("Failed to find module for LoRA layer key: %s", layer_key) + continue + + # Decide whether to use direct patching or a sidecar patch. + # Direct patching is preferred, because it results in better runtime speed. + # Reasons to use sidecar patching: + # - The module is quantized, so the caller passed force_sidecar_patching=True. + # - The module already has sidecar patches. + # - The module is on the CPU (and we don't want to store a second full copy of the original weights on the + # CPU, since this would double the RAM usage) + # NOTE: For now, we don't check if the layer is quantized here. We assume that this is checked in the caller + # and that the caller will set force_sidecar_patching=True if the layer is quantized. + # TODO(ryand): Handle the case where we are running without a GPU. Should we set a config flag that allows + # forcing full patching even on the CPU? + use_sidecar_patching = False + if force_direct_patching and force_sidecar_patching: + raise ValueError("Cannot force both direct and sidecar patching.") + elif force_sidecar_patching: + use_sidecar_patching = True + elif LayerPatcher._is_any_part_of_layer_fp8(module): + # FP8 weights (e.g. a model loaded with fp8_storage layerwise casting) cannot be + # directly patched: _apply_model_layer_patch does an in-place add on the model weight, + # and CUDA has no add kernel for float8 ("ufunc_add_CUDA not implemented for + # Float8_e4m3fn"). Sidecar patching dequantizes to the compute dtype before any math, + # so it works regardless of the storage dtype. This takes precedence over + # force_direct_patching, since direct patching is simply not possible on fp8 weights. + use_sidecar_patching = True + elif force_direct_patching: + use_sidecar_patching = False + elif module.get_num_patches() > 0: + use_sidecar_patching = True + elif LayerPatcher._is_any_part_of_layer_on_cpu(module): + use_sidecar_patching = True + + if use_sidecar_patching: + LayerPatcher._apply_model_layer_wrapper_patch( + module_to_patch=module, + module_to_patch_key=module_key, + patch=layer, + patch_weight=patch_weight, + original_modules=original_modules, + dtype=dtype, + ) + else: + LayerPatcher._apply_model_layer_patch( + module_to_patch=module, + module_to_patch_key=module_key, + patch=layer, + patch_weight=patch_weight, + original_weights=original_weights, + ) + + @staticmethod + def _is_any_part_of_layer_on_cpu(layer: torch.nn.Module) -> bool: + return any(p.device.type == "cpu" for p in layer.parameters()) + + # FP8 storage dtypes. Direct patching does in-place arithmetic on the model weights, which has no + # CUDA kernel for these dtypes, so a layer with fp8 weights must be patched via the sidecar wrapper. + _FP8_DTYPES = (torch.float8_e4m3fn, torch.float8_e5m2) + + @staticmethod + def _is_any_part_of_layer_fp8(layer: torch.nn.Module) -> bool: + return any(p.dtype in LayerPatcher._FP8_DTYPES for p in layer.parameters()) + + @staticmethod + @torch.no_grad() + def _apply_model_layer_patch( + module_to_patch: torch.nn.Module, + module_to_patch_key: str, + patch: BaseLayerPatch, + patch_weight: float, + original_weights: OriginalWeightsStorage, + ): + # All of the LoRA weight calculations will be done on the same device as the module weight. + # (Performance will be best if this is a CUDA device.) + first_param = next(module_to_patch.parameters()) + device = first_param.device + dtype = first_param.dtype + + # We intentionally move to the target device first, then cast. Experimentally, this was found to + # be significantly faster for 16-bit CPU tensors being moved to a CUDA device than doing the + # same thing in a single call to '.to(...)'. + patch.to(device=device) + patch.to(dtype=torch.float32) + + # TODO(ryand): Using torch.autocast(...) over explicit casting may offer a speed benefit on CUDA + # devices here. Experimentally, it was found to be very slow on CPU. More investigation needed. + params_dict = patch.get_parameters(dict(module_to_patch.named_parameters(recurse=False)), weight=patch_weight) + if not params_dict: + logger = InvokeAILogger.get_logger(LayerPatcher.__name__) + logger.warning(f"LoRA patch returned no parameters for module: {module_to_patch_key}") + return + + for param_name, param_weight in params_dict.items(): + param_key = module_to_patch_key + "." + param_name + module_param = module_to_patch.get_parameter(param_name) + + # Save original weight + original_weights.save(param_key, module_param) + + # Handle layers that change the shape of the original layer. + # FLUX control LoRAs intentionally expand certain layers - we pad the original weight with zeros. + # For other LoRAs (e.g., Z-Image with architecture mismatch), skip incompatible layers with a warning. + if module_param.nelement() != param_weight.nelement(): + if isinstance(patch, FluxControlLoRALayer): + # FLUX Control LoRAs intentionally expand layers - pad with zeros + expanded_weight = pad_with_zeros(module_param, param_weight.shape) + setattr( + module_to_patch, + param_name, + torch.nn.Parameter(expanded_weight, requires_grad=module_param.requires_grad), + ) + module_param = expanded_weight + else: + # For other LoRAs, shape mismatch indicates architecture incompatibility - skip the layer + logger = InvokeAILogger.get_logger(LayerPatcher.__name__) + logger.warning( + f"Skipping LoRA layer '{module_to_patch_key}.{param_name}' due to shape mismatch: " + f"model has {module_param.nelement()} elements, LoRA expects {param_weight.nelement()}. " + "This LoRA may be incompatible with this model architecture." + ) + continue + + # Convert param_weight to the correct device and dtype, then apply to model weights + param_weight_converted = param_weight.to(device=device, dtype=dtype) + module_param.data.copy_(module_param.data + param_weight_converted) + + patch.to(device=TorchDevice.CPU_DEVICE) + + @staticmethod + @torch.no_grad() + def _apply_model_layer_wrapper_patch( + module_to_patch: torch.nn.Module, + module_to_patch_key: str, + patch: BaseLayerPatch, + patch_weight: float, + original_modules: dict[str, torch.nn.Module], + dtype: torch.dtype, + ): + """Apply a single LoRA wrapper patch to a module.""" + # Move the LoRA layer to the same device/dtype as the orig module. + first_param = next(module_to_patch.parameters()) + device = first_param.device + patch.to(device=device, dtype=dtype) + + if module_to_patch_key not in original_modules: + original_modules[module_to_patch_key] = module_to_patch + + module_to_patch.add_patch(patch, patch_weight) + + @staticmethod + def _split_parent_key(module_key: str) -> tuple[str, str]: + """Split a module key into its parent key and module name. + + Args: + module_key (str): The module key to split. + + Returns: + tuple[str, str]: A tuple containing the parent key and module name. + """ + split_key = module_key.rsplit(".", 1) + if len(split_key) == 2: + return tuple(split_key) + elif len(split_key) == 1: + return "", split_key[0] + else: + raise ValueError(f"Invalid module key: {module_key}") + + @staticmethod + def _set_submodule(parent_module: torch.nn.Module, module_name: str, submodule: torch.nn.Module): + try: + submodule_index = int(module_name) + # If the module name is an integer, then we use the __setitem__ method to set the submodule. + parent_module[submodule_index] = submodule # type: ignore + except ValueError: + # If the module name is not an integer, then we use the setattr method to set the submodule. + setattr(parent_module, module_name, submodule) + + @staticmethod + def _get_submodule( + model: torch.nn.Module, layer_key: str, layer_key_is_flattened: bool + ) -> tuple[str, torch.nn.Module]: + """Get the submodule corresponding to the given layer key. + + Args: + model (torch.nn.Module): The model to search. + layer_key (str): The layer key to search for. + layer_key_is_flattened (bool): Whether the layer key is flattened. If flattened, then all '.' have been + replaced with '_'. Non-flattened keys are preferred, because they allow submodules to be accessed + directly without searching, but some legacy code still uses flattened keys. + + Returns: + tuple[str, torch.nn.Module]: A tuple containing the module key and the submodule. + """ + if not layer_key_is_flattened: + return layer_key, model.get_submodule(layer_key) + + # Handle flattened keys. + assert "." not in layer_key + + module = model + module_key = "" + key_parts = layer_key.split("_") + + submodule_name = key_parts.pop(0) + + while len(key_parts) > 0: + try: + module = module.get_submodule(submodule_name) + module_key += "." + submodule_name + submodule_name = key_parts.pop(0) + except Exception: + submodule_name += "_" + key_parts.pop(0) + + module = module.get_submodule(submodule_name) + module_key = (module_key + "." + submodule_name).lstrip(".") + + return module_key, module diff --git a/invokeai/backend/patches/layers/__init__.py b/invokeai/backend/patches/layers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/patches/layers/base_layer_patch.py b/invokeai/backend/patches/layers/base_layer_patch.py new file mode 100644 index 00000000000..f6f0289a906 --- /dev/null +++ b/invokeai/backend/patches/layers/base_layer_patch.py @@ -0,0 +1,22 @@ +from abc import ABC, abstractmethod + +import torch + + +class BaseLayerPatch(ABC): + @abstractmethod + def get_parameters(self, orig_parameters: dict[str, torch.Tensor], weight: float) -> dict[str, torch.Tensor]: + """Get the parameter residual updates that should be applied to the original parameters. Parameters omitted + from the returned dict are not updated. + """ + ... + + @abstractmethod + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + """Move all internal tensors to the specified device and dtype.""" + ... + + @abstractmethod + def calc_size(self) -> int: + """Calculate the total size of all internal tensors in bytes.""" + ... diff --git a/invokeai/backend/patches/layers/dora_layer.py b/invokeai/backend/patches/layers/dora_layer.py new file mode 100644 index 00000000000..3e52ce95783 --- /dev/null +++ b/invokeai/backend/patches/layers/dora_layer.py @@ -0,0 +1,115 @@ +from typing import Dict, Optional + +import torch + +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device +from invokeai.backend.patches.layers.lora_layer_base import LoRALayerBase +from invokeai.backend.util.calc_tensor_size import calc_tensors_size + + +class DoRALayer(LoRALayerBase): + """A DoRA layer. As defined in https://arxiv.org/pdf/2402.09353.""" + + def __init__( + self, + up: torch.Tensor, + down: torch.Tensor, + dora_scale: torch.Tensor, + alpha: float | None, + bias: Optional[torch.Tensor], + ): + super().__init__(alpha, bias) + self.up = up + self.down = down + self.dora_scale = dora_scale + + @classmethod + def from_state_dict_values(cls, values: Dict[str, torch.Tensor]): + alpha = cls._parse_alpha(values.get("alpha", None)) + bias = cls._parse_bias( + values.get("bias_indices", None), values.get("bias_values", None), values.get("bias_size", None) + ) + + layer = cls( + up=values["lora_up.weight"], + down=values["lora_down.weight"], + dora_scale=values["dora_scale"], + alpha=alpha, + bias=bias, + ) + + cls.warn_on_unhandled_keys( + values=values, + handled_keys={ + # Default keys. + "alpha", + "bias_indices", + "bias_values", + "bias_size", + # Layer-specific keys. + "lora_up.weight", + "lora_down.weight", + "dora_scale", + }, + ) + + return layer + + def _rank(self) -> int: + return self.down.shape[0] + + def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor: + orig_weight = cast_to_device(orig_weight, self.up.device) + + # Note: Variable names (e.g. delta_v) are based on the paper. + delta_v = self.up.reshape(self.up.shape[0], -1) @ self.down.reshape(self.down.shape[0], -1) + delta_v = delta_v.reshape(orig_weight.shape) + + delta_v = delta_v * self.scale() + + # At this point, out_weight is the unnormalized direction matrix. + out_weight = orig_weight + delta_v + + # TODO(ryand): Simplify this logic. + direction_norm = ( + out_weight.transpose(0, 1) + .reshape(out_weight.shape[1], -1) + .norm(dim=1, keepdim=True) + .reshape(out_weight.shape[1], *[1] * (out_weight.dim() - 1)) + .transpose(0, 1) + ) + + out_weight *= self.dora_scale / direction_norm + + return out_weight - orig_weight + + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + super().to(device=device, dtype=dtype) + self.up = self.up.to(device=device, dtype=dtype) + self.down = self.down.to(device=device, dtype=dtype) + self.dora_scale = self.dora_scale.to(device=device, dtype=dtype) + + def calc_size(self) -> int: + return super().calc_size() + calc_tensors_size([self.up, self.down, self.dora_scale]) + + def get_parameters(self, orig_parameters: dict[str, torch.Tensor], weight: float) -> dict[str, torch.Tensor]: + if any(p.device.type == "meta" for p in orig_parameters.values()): + # If any of the original parameters are on the 'meta' device, we assume this is because the base model is in + # a quantization format that doesn't allow easy dequantization. + raise RuntimeError( + "The base model quantization format (likely bitsandbytes) is not compatible with DoRA patches." + ) + + scale = self.scale() + params = {"weight": self.get_weight(orig_parameters["weight"]) * weight} + bias = self.get_bias(orig_parameters.get("bias", None)) + if bias is not None: + params["bias"] = bias * (weight * scale) + + # Reshape all params to match the original module's shape. + for param_name, param_weight in params.items(): + orig_param = orig_parameters[param_name] + if param_weight.shape != orig_param.shape: + params[param_name] = param_weight.reshape(orig_param.shape) + + return params diff --git a/invokeai/backend/patches/layers/flux_control_lora_layer.py b/invokeai/backend/patches/layers/flux_control_lora_layer.py new file mode 100644 index 00000000000..ad592456a9d --- /dev/null +++ b/invokeai/backend/patches/layers/flux_control_lora_layer.py @@ -0,0 +1,19 @@ +import torch + +from invokeai.backend.patches.layers.lora_layer import LoRALayer + + +class FluxControlLoRALayer(LoRALayer): + """A special case of LoRALayer for use with FLUX Control LoRAs that pads the target parameter with zeros if the + shapes don't match. + """ + + def get_parameters(self, orig_parameters: dict[str, torch.Tensor], weight: float) -> dict[str, torch.Tensor]: + """This overrides the base class behavior to skip the reshaping step.""" + scale = self.scale() + params = {"weight": self.get_weight(orig_parameters["weight"]) * (weight * scale)} + bias = self.get_bias(orig_parameters.get("bias", None)) + if bias is not None: + params["bias"] = bias * (weight * scale) + + return params diff --git a/invokeai/backend/patches/layers/full_layer.py b/invokeai/backend/patches/layers/full_layer.py new file mode 100644 index 00000000000..84e06058e85 --- /dev/null +++ b/invokeai/backend/patches/layers/full_layer.py @@ -0,0 +1,34 @@ +from typing import Dict, Optional + +import torch + +from invokeai.backend.patches.layers.lora_layer_base import LoRALayerBase +from invokeai.backend.util.calc_tensor_size import calc_tensor_size + + +class FullLayer(LoRALayerBase): + def __init__(self, weight: torch.Tensor, bias: Optional[torch.Tensor]): + super().__init__(alpha=None, bias=bias) + self.weight = torch.nn.Parameter(weight) + + @classmethod + def from_state_dict_values( + cls, + values: Dict[str, torch.Tensor], + ): + layer = cls(weight=values["diff"], bias=values.get("diff_b", None)) + cls.warn_on_unhandled_keys(values=values, handled_keys={"diff", "diff_b"}) + return layer + + def _rank(self) -> int | None: + return None + + def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor: + return self.weight + + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + super().to(device=device, dtype=dtype) + self.weight = self.weight.to(device=device, dtype=dtype) + + def calc_size(self) -> int: + return super().calc_size() + calc_tensor_size(self.weight) diff --git a/invokeai/backend/patches/layers/ia3_layer.py b/invokeai/backend/patches/layers/ia3_layer.py new file mode 100644 index 00000000000..21c84669836 --- /dev/null +++ b/invokeai/backend/patches/layers/ia3_layer.py @@ -0,0 +1,59 @@ +from typing import Dict, Optional + +import torch + +from invokeai.backend.model_manager.load.model_cache.torch_module_autocast.cast_to_device import cast_to_device +from invokeai.backend.patches.layers.lora_layer_base import LoRALayerBase + + +class IA3Layer(LoRALayerBase): + """IA3 Layer + + Example model for testing this layer type: https://civitai.com/models/123930/gwendolyn-tennyson-ben-10-ia3 + """ + + def __init__(self, weight: torch.Tensor, on_input: torch.Tensor, bias: Optional[torch.Tensor]): + super().__init__(alpha=None, bias=bias) + self.weight = weight + self.on_input = on_input + + def _rank(self) -> int | None: + return None + + @classmethod + def from_state_dict_values( + cls, + values: Dict[str, torch.Tensor], + ): + bias = cls._parse_bias( + values.get("bias_indices", None), values.get("bias_values", None), values.get("bias_size", None) + ) + layer = cls( + weight=values["weight"], + on_input=values["on_input"], + bias=bias, + ) + cls.warn_on_unhandled_keys( + values=values, + handled_keys={ + # Default keys. + "bias_indices", + "bias_values", + "bias_size", + # Layer-specific keys. + "weight", + "on_input", + }, + ) + return layer + + def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor: + weight = self.weight + if not self.on_input: + weight = weight.reshape(-1, 1) + return cast_to_device(orig_weight, weight.device) * weight + + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + super().to(device, dtype) + self.weight = self.weight.to(device, dtype) + self.on_input = self.on_input.to(device, dtype) diff --git a/invokeai/backend/patches/layers/loha_layer.py b/invokeai/backend/patches/layers/loha_layer.py new file mode 100644 index 00000000000..d337a318834 --- /dev/null +++ b/invokeai/backend/patches/layers/loha_layer.py @@ -0,0 +1,98 @@ +from typing import Dict + +import torch + +from invokeai.backend.patches.layers.lora_layer_base import LoRALayerBase +from invokeai.backend.util.calc_tensor_size import calc_tensors_size + + +class LoHALayer(LoRALayerBase): + """LoHA LyCoris layer. + + Example model for testing this layer type: https://civitai.com/models/27397/loha-renoir-the-dappled-light-style + """ + + def __init__( + self, + w1_a: torch.Tensor, + w1_b: torch.Tensor, + w2_a: torch.Tensor, + w2_b: torch.Tensor, + t1: torch.Tensor | None, + t2: torch.Tensor | None, + alpha: float | None, + bias: torch.Tensor | None, + ): + super().__init__(alpha=alpha, bias=bias) + self.w1_a = w1_a + self.w1_b = w1_b + self.w2_a = w2_a + self.w2_b = w2_b + self.t1 = t1 + self.t2 = t2 + assert (self.t1 is None) == (self.t2 is None) + + def _rank(self) -> int | None: + return self.w1_b.shape[0] + + @classmethod + def from_state_dict_values( + cls, + values: Dict[str, torch.Tensor], + ): + alpha = cls._parse_alpha(values.get("alpha", None)) + bias = cls._parse_bias( + values.get("bias_indices", None), values.get("bias_values", None), values.get("bias_size", None) + ) + layer = cls( + w1_a=values["hada_w1_a"], + w1_b=values["hada_w1_b"], + w2_a=values["hada_w2_a"], + w2_b=values["hada_w2_b"], + t1=values.get("hada_t1", None), + t2=values.get("hada_t2", None), + alpha=alpha, + bias=bias, + ) + + cls.warn_on_unhandled_keys( + values=values, + handled_keys={ + # Default keys. + "alpha", + "bias_indices", + "bias_values", + "bias_size", + # Layer-specific keys. + "hada_w1_a", + "hada_w1_b", + "hada_w2_a", + "hada_w2_b", + "hada_t1", + "hada_t2", + }, + ) + + return layer + + def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor: + if self.t1 is None: + weight: torch.Tensor = (self.w1_a @ self.w1_b) * (self.w2_a @ self.w2_b) + else: + rebuild1 = torch.einsum("i j k l, j r, i p -> p r k l", self.t1, self.w1_b, self.w1_a) + rebuild2 = torch.einsum("i j k l, j r, i p -> p r k l", self.t2, self.w2_b, self.w2_a) + weight = rebuild1 * rebuild2 + + return weight + + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + super().to(device=device, dtype=dtype) + self.w1_a = self.w1_a.to(device=device, dtype=dtype) + self.w1_b = self.w1_b.to(device=device, dtype=dtype) + self.w2_a = self.w2_a.to(device=device, dtype=dtype) + self.w2_b = self.w2_b.to(device=device, dtype=dtype) + self.t1 = self.t1.to(device=device, dtype=dtype) if self.t1 is not None else self.t1 + self.t2 = self.t2.to(device=device, dtype=dtype) if self.t2 is not None else self.t2 + + def calc_size(self) -> int: + return super().calc_size() + calc_tensors_size([self.w1_a, self.w1_b, self.w2_a, self.w2_b, self.t1, self.t2]) diff --git a/invokeai/backend/patches/layers/lokr_layer.py b/invokeai/backend/patches/layers/lokr_layer.py new file mode 100644 index 00000000000..e33d80d2738 --- /dev/null +++ b/invokeai/backend/patches/layers/lokr_layer.py @@ -0,0 +1,127 @@ +from typing import Dict + +import torch + +from invokeai.backend.patches.layers.lora_layer_base import LoRALayerBase +from invokeai.backend.util.calc_tensor_size import calc_tensors_size + + +class LoKRLayer(LoRALayerBase): + """LoKR LyCoris layer. + + Example model for testing this layer type: https://civitai.com/models/346747/lokrnekopara-allgirl-for-jru2 + """ + + def __init__( + self, + w1: torch.Tensor | None, + w1_a: torch.Tensor | None, + w1_b: torch.Tensor | None, + w2: torch.Tensor | None, + w2_a: torch.Tensor | None, + w2_b: torch.Tensor | None, + t2: torch.Tensor | None, + alpha: float | None, + bias: torch.Tensor | None, + ): + super().__init__(alpha=alpha, bias=bias) + self.w1 = w1 + self.w1_a = w1_a + self.w1_b = w1_b + self.w2 = w2 + self.w2_a = w2_a + self.w2_b = w2_b + self.t2 = t2 + + # Validate parameters. + assert (self.w1 is None) != (self.w1_a is None) + assert (self.w1_a is None) == (self.w1_b is None) + assert (self.w2 is None) != (self.w2_a is None) + assert (self.w2_a is None) == (self.w2_b is None) + + def _rank(self) -> int | None: + if self.w1_b is not None: + return self.w1_b.shape[0] + elif self.w2_b is not None: + return self.w2_b.shape[0] + else: + return None + + @classmethod + def from_state_dict_values( + cls, + values: Dict[str, torch.Tensor], + ): + alpha = cls._parse_alpha(values.get("alpha", None)) + bias = cls._parse_bias( + values.get("bias_indices", None), values.get("bias_values", None), values.get("bias_size", None) + ) + layer = cls( + w1=values.get("lokr_w1", None), + w1_a=values.get("lokr_w1_a", None), + w1_b=values.get("lokr_w1_b", None), + w2=values.get("lokr_w2", None), + w2_a=values.get("lokr_w2_a", None), + w2_b=values.get("lokr_w2_b", None), + t2=values.get("lokr_t2", None), + alpha=alpha, + bias=bias, + ) + + cls.warn_on_unhandled_keys( + values, + { + # Default keys. + "alpha", + "bias_indices", + "bias_values", + "bias_size", + # Layer-specific keys. + "lokr_w1", + "lokr_w1_a", + "lokr_w1_b", + "lokr_w2", + "lokr_w2_a", + "lokr_w2_b", + "lokr_t2", + }, + ) + + return layer + + def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor: + w1 = self.w1 + if w1 is None: + assert self.w1_a is not None + assert self.w1_b is not None + w1 = self.w1_a @ self.w1_b + + w2 = self.w2 + if w2 is None: + if self.t2 is None: + assert self.w2_a is not None + assert self.w2_b is not None + w2 = self.w2_a @ self.w2_b + else: + w2 = torch.einsum("i j k l, i p, j r -> p r k l", self.t2, self.w2_a, self.w2_b) + + if len(w2.shape) == 4: + w1 = w1.unsqueeze(2).unsqueeze(2) + w2 = w2.contiguous() + weight = torch.kron(w1, w2) + return weight + + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + super().to(device=device, dtype=dtype) + self.w1 = self.w1.to(device=device, dtype=dtype) if self.w1 is not None else self.w1 + self.w1_a = self.w1_a.to(device=device, dtype=dtype) if self.w1_a is not None else self.w1_a + self.w1_b = self.w1_b.to(device=device, dtype=dtype) if self.w1_b is not None else self.w1_b + self.w2 = self.w2.to(device=device, dtype=dtype) if self.w2 is not None else self.w2 + self.w2_a = self.w2_a.to(device=device, dtype=dtype) if self.w2_a is not None else self.w2_a + self.w2_b = self.w2_b.to(device=device, dtype=dtype) if self.w2_b is not None else self.w2_b + self.t2 = self.t2.to(device=device, dtype=dtype) if self.t2 is not None else self.t2 + + def calc_size(self) -> int: + return super().calc_size() + calc_tensors_size( + [self.w1, self.w1_a, self.w1_b, self.w2, self.w2_a, self.w2_b, self.t2] + ) diff --git a/invokeai/backend/patches/layers/lora_layer.py b/invokeai/backend/patches/layers/lora_layer.py new file mode 100644 index 00000000000..cf79f520519 --- /dev/null +++ b/invokeai/backend/patches/layers/lora_layer.py @@ -0,0 +1,110 @@ +from typing import Dict, Optional + +import torch + +from invokeai.backend.patches.layers.lora_layer_base import LoRALayerBase +from invokeai.backend.util.calc_tensor_size import calc_tensors_size + + +class LoRALayer(LoRALayerBase): + def __init__( + self, + up: torch.Tensor, + mid: Optional[torch.Tensor], + down: torch.Tensor, + alpha: float | None, + bias: Optional[torch.Tensor], + ): + super().__init__(alpha, bias) + self.up = up + self.mid = mid + self.down = down + self.are_ranks_equal = up.shape[1] == down.shape[0] + + @classmethod + def from_state_dict_values( + cls, + values: Dict[str, torch.Tensor], + ): + alpha = cls._parse_alpha(values.get("alpha", None)) + bias = cls._parse_bias( + values.get("bias_indices", None), values.get("bias_values", None), values.get("bias_size", None) + ) + + layer = cls( + up=values["lora_up.weight"], + down=values["lora_down.weight"], + mid=values.get("lora_mid.weight", None), + alpha=alpha, + bias=bias, + ) + + cls.warn_on_unhandled_keys( + values=values, + handled_keys={ + # Default keys. + "alpha", + "bias_indices", + "bias_values", + "bias_size", + # Layer-specific keys. + "lora_up.weight", + "lora_down.weight", + "lora_mid.weight", + }, + ) + + return layer + + def _rank(self) -> int: + return self.down.shape[0] + + def fuse_weights(self, up: torch.Tensor, down: torch.Tensor) -> torch.Tensor: + """ + Fuse the weights of the up and down matrices of a LoRA layer with different ranks. + + Since the Huggingface implementation of KQV projections are fused, when we convert to Kohya format + the LoRA weights have different ranks. This function handles the fusion of these differently sized + matrices. + """ + + fused_lora = torch.zeros((up.shape[0], down.shape[1]), device=down.device, dtype=down.dtype) + rank_diff = down.shape[0] / up.shape[1] + + if rank_diff > 1: + rank_diff = down.shape[0] / up.shape[1] + w_down = down.chunk(int(rank_diff), dim=0) + for w_down_chunk in w_down: + fused_lora = fused_lora + (torch.mm(up, w_down_chunk)) + else: + rank_diff = up.shape[1] / down.shape[0] + w_up = up.chunk(int(rank_diff), dim=0) + for w_up_chunk in w_up: + fused_lora = fused_lora + (torch.mm(w_up_chunk, down)) + + return fused_lora + + def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor: + if self.mid is not None: + up = self.up.reshape(self.up.shape[0], self.up.shape[1]) + down = self.down.reshape(self.down.shape[0], self.down.shape[1]) + weight = torch.einsum("m n w h, i m, n j -> i j w h", self.mid, up, down) + else: + # up matrix and down matrix have different ranks so we can't simply multiply them + if not self.are_ranks_equal: + weight = self.fuse_weights(self.up, self.down) + return weight + + weight = self.up.reshape(self.up.shape[0], -1) @ self.down.reshape(self.down.shape[0], -1) + + return weight + + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + super().to(device=device, dtype=dtype) + self.up = self.up.to(device=device, dtype=dtype) + if self.mid is not None: + self.mid = self.mid.to(device=device, dtype=dtype) + self.down = self.down.to(device=device, dtype=dtype) + + def calc_size(self) -> int: + return super().calc_size() + calc_tensors_size([self.up, self.mid, self.down]) diff --git a/invokeai/backend/patches/layers/lora_layer_base.py b/invokeai/backend/patches/layers/lora_layer_base.py new file mode 100644 index 00000000000..099efe3bed8 --- /dev/null +++ b/invokeai/backend/patches/layers/lora_layer_base.py @@ -0,0 +1,91 @@ +from typing import Optional + +import torch + +import invokeai.backend.util.logging as logger +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.param_shape_utils import get_param_shape +from invokeai.backend.util.calc_tensor_size import calc_tensors_size + + +class LoRALayerBase(BaseLayerPatch): + """Base class for all LoRA-like patching layers.""" + + # Note: It is tempting to make this a torch.nn.Module sub-class and make all tensors 'torch.nn.Parameter's. Then we + # could inherit automatic .to(...) behavior for this class, its subclasses, and all sidecar layers that wrap a + # LoRALayerBase. We would also be able to implement a single calc_size() method that could be inherited by all + # subclasses. But, it turns out that the speed overhead of the default .to(...) implementation in torch.nn.Module is + # noticeable, so for now we have opted not to use torch.nn.Module. + + def __init__(self, alpha: float | None, bias: torch.Tensor | None): + self._alpha = alpha + self.bias = bias + + @classmethod + def _parse_bias( + cls, bias_indices: torch.Tensor | None, bias_values: torch.Tensor | None, bias_size: torch.Tensor | None + ) -> torch.Tensor | None: + """Helper function to parse a bias tensor from a state dict in LyCORIS format.""" + assert (bias_indices is None) == (bias_values is None) == (bias_size is None) + + bias = None + if bias_indices is not None: + bias = torch.sparse_coo_tensor(bias_indices, bias_values, tuple(bias_size)) + return bias + + @classmethod + def _parse_alpha( + cls, + alpha: torch.Tensor | None, + ) -> float | None: + return alpha.item() if alpha is not None else None + + def _rank(self) -> int | None: + """Return the rank of the LoRA-like layer. Or None if the layer does not have a rank. This value is used to + calculate the scale. + """ + raise NotImplementedError() + + def scale(self) -> float: + rank = self._rank() + if self._alpha is None or rank is None: + return 1.0 + return self._alpha / rank + + def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor: + raise NotImplementedError() + + def get_bias(self, orig_bias: torch.Tensor | None) -> Optional[torch.Tensor]: + return self.bias + + def get_parameters(self, orig_parameters: dict[str, torch.Tensor], weight: float) -> dict[str, torch.Tensor]: + scale = self.scale() + lora_weight = self.get_weight(orig_parameters["weight"]) + params = {"weight": lora_weight * (weight * scale)} + bias = self.get_bias(orig_parameters.get("bias", None)) + if bias is not None: + params["bias"] = bias * (weight * scale) + + # Reshape all params to match the original module's shape. + for param_name, param_weight in params.items(): + orig_param = orig_parameters[param_name] + if param_weight.shape != get_param_shape(orig_param): + params[param_name] = param_weight.reshape(get_param_shape(orig_param)) + + return params + + @classmethod + def warn_on_unhandled_keys(cls, values: dict[str, torch.Tensor], handled_keys: set[str]): + """Log a warning if values contains unhandled keys.""" + unknown_keys = set(values.keys()) - handled_keys + if unknown_keys: + logger.warning( + f"Unexpected keys found in LoRA/LyCORIS layer, model might work incorrectly! Unexpected keys: {unknown_keys}" + ) + + def calc_size(self) -> int: + return calc_tensors_size([self.bias]) + + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + if self.bias is not None: + self.bias = self.bias.to(device=device, dtype=dtype) diff --git a/invokeai/backend/patches/layers/merged_layer_patch.py b/invokeai/backend/patches/layers/merged_layer_patch.py new file mode 100644 index 00000000000..ec2039e746c --- /dev/null +++ b/invokeai/backend/patches/layers/merged_layer_patch.py @@ -0,0 +1,65 @@ +from dataclasses import dataclass +from typing import Sequence + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.param_shape_utils import get_param_shape + + +@dataclass +class Range: + start: int + end: int + + +class MergedLayerPatch(BaseLayerPatch): + """A patch layer that is composed of multiple sub-layers merged together. + + This class was created to handle a special case with FLUX LoRA models. In the BFL FLUX model format, the attention + Q, K, V matrices are concatenated along the first dimension. In the diffusers LoRA format, the Q, K, V matrices are + stored as separate tensors. This class enables diffusers LoRA layers to be used in BFL FLUX models. + """ + + def __init__( + self, + lora_layers: Sequence[BaseLayerPatch], + ranges: Sequence[Range], + ): + super().__init__() + + self.lora_layers = lora_layers + # self.ranges[i] is the range for the i'th lora layer along the 0'th weight dimension. + self.ranges = ranges + assert len(self.ranges) == len(self.lora_layers) + + def get_parameters(self, orig_parameters: dict[str, torch.Tensor], weight: float) -> dict[str, torch.Tensor]: + out_parameters: dict[str, torch.Tensor] = {} + + for lora_layer, range in zip(self.lora_layers, self.ranges, strict=True): + sliced_parameters: dict[str, torch.Tensor] = { + n: p[range.start : range.end] for n, p in orig_parameters.items() + } + + # Note that `weight` is applied in the sub-layers, no need to apply it in this function. + layer_out_parameters = lora_layer.get_parameters(sliced_parameters, weight) + + for out_param_name, out_param in layer_out_parameters.items(): + if out_param_name not in out_parameters: + # If not already in the output dict, initialize an output tensor with the same shape as the full + # original parameter. + out_parameters[out_param_name] = torch.zeros( + get_param_shape(orig_parameters[out_param_name]), + dtype=out_param.dtype, + device=out_param.device, + ) + out_parameters[out_param_name][range.start : range.end] += out_param + + return out_parameters + + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + for lora_layer in self.lora_layers: + lora_layer.to(device=device, dtype=dtype) + + def calc_size(self) -> int: + return sum(lora_layer.calc_size() for lora_layer in self.lora_layers) diff --git a/invokeai/backend/patches/layers/norm_layer.py b/invokeai/backend/patches/layers/norm_layer.py new file mode 100644 index 00000000000..5de6e028d22 --- /dev/null +++ b/invokeai/backend/patches/layers/norm_layer.py @@ -0,0 +1,34 @@ +from typing import Dict + +import torch + +from invokeai.backend.patches.layers.lora_layer_base import LoRALayerBase +from invokeai.backend.util.calc_tensor_size import calc_tensor_size + + +class NormLayer(LoRALayerBase): + def __init__(self, weight: torch.Tensor, bias: torch.Tensor | None): + super().__init__(alpha=None, bias=bias) + self.weight = weight + + @classmethod + def from_state_dict_values( + cls, + values: Dict[str, torch.Tensor], + ): + layer = cls(weight=values["w_norm"], bias=values.get("b_norm", None)) + cls.warn_on_unhandled_keys(values, {"w_norm", "b_norm"}) + return layer + + def _rank(self) -> int | None: + return None + + def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor: + return self.weight + + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + super().to(device=device, dtype=dtype) + self.weight = self.weight.to(device=device, dtype=dtype) + + def calc_size(self) -> int: + return super().calc_size() + calc_tensor_size(self.weight) diff --git a/invokeai/backend/patches/layers/param_shape_utils.py b/invokeai/backend/patches/layers/param_shape_utils.py new file mode 100644 index 00000000000..4eab1daaa41 --- /dev/null +++ b/invokeai/backend/patches/layers/param_shape_utils.py @@ -0,0 +1,19 @@ +import torch + +try: + from bitsandbytes.nn.modules import Params4bit + + bnb_available: bool = True +except ImportError: + bnb_available: bool = False + + +def get_param_shape(param: torch.Tensor) -> torch.Size: + """A helper function to get the shape of a parameter that handles `bitsandbytes.nn.Params4Bit` correctly.""" + # Accessing the `.shape` attribute of `bitsandbytes.nn.Params4Bit` will return an incorrect result. Instead, we must + # access the `.quant_state.shape` attribute. + if bnb_available and type(param) is Params4bit: # type: ignore + quant_state = param.quant_state + if quant_state is not None: + return quant_state.shape + return param.shape diff --git a/invokeai/backend/patches/layers/set_parameter_layer.py b/invokeai/backend/patches/layers/set_parameter_layer.py new file mode 100644 index 00000000000..1b7fe94d366 --- /dev/null +++ b/invokeai/backend/patches/layers/set_parameter_layer.py @@ -0,0 +1,27 @@ +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.util.calc_tensor_size import calc_tensor_size + + +class SetParameterLayer(BaseLayerPatch): + """A layer that sets a single parameter to a new target value. + (The diff between the target value and current value is calculated internally.) + """ + + def __init__(self, param_name: str, weight: torch.Tensor): + super().__init__() + self.weight = weight + self.param_name = param_name + + def get_parameters(self, orig_parameters: dict[str, torch.Tensor], weight: float) -> dict[str, torch.Tensor]: + # Note: We intentionally ignore the weight parameter here. This matches the behavior in the official FLUX + # Control LoRA implementation. + diff = self.weight - orig_parameters[self.param_name] + return {self.param_name: diff} + + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + self.weight = self.weight.to(device=device, dtype=dtype) + + def calc_size(self) -> int: + return calc_tensor_size(self.weight) diff --git a/invokeai/backend/patches/layers/utils.py b/invokeai/backend/patches/layers/utils.py new file mode 100644 index 00000000000..8141a56644a --- /dev/null +++ b/invokeai/backend/patches/layers/utils.py @@ -0,0 +1,35 @@ +from typing import Dict + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.dora_layer import DoRALayer +from invokeai.backend.patches.layers.full_layer import FullLayer +from invokeai.backend.patches.layers.ia3_layer import IA3Layer +from invokeai.backend.patches.layers.loha_layer import LoHALayer +from invokeai.backend.patches.layers.lokr_layer import LoKRLayer +from invokeai.backend.patches.layers.lora_layer import LoRALayer +from invokeai.backend.patches.layers.norm_layer import NormLayer + + +def any_lora_layer_from_state_dict(state_dict: Dict[str, torch.Tensor]) -> BaseLayerPatch: + # Detect layers according to LyCORIS detection logic(`weight_list_det`) + # https://github.com/KohakuBlueleaf/LyCORIS/tree/8ad8000efb79e2b879054da8c9356e6143591bad/lycoris/modules + if "dora_scale" in state_dict: + return DoRALayer.from_state_dict_values(state_dict) + elif "lora_up.weight" in state_dict: + # LoRA a.k.a LoCon + return LoRALayer.from_state_dict_values(state_dict) + elif "hada_w1_a" in state_dict: + return LoHALayer.from_state_dict_values(state_dict) + elif "lokr_w1" in state_dict or "lokr_w1_a" in state_dict: + return LoKRLayer.from_state_dict_values(state_dict) + elif "diff" in state_dict: + # Full a.k.a Diff + return FullLayer.from_state_dict_values(state_dict) + elif "on_input" in state_dict: + return IA3Layer.from_state_dict_values(state_dict) + elif "w_norm" in state_dict: + return NormLayer.from_state_dict_values(state_dict) + else: + raise ValueError(f"Unsupported lora format: {state_dict.keys()}") diff --git a/invokeai/backend/patches/lora_conversions/__init__.py b/invokeai/backend/patches/lora_conversions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/patches/lora_conversions/anima_lora_constants.py b/invokeai/backend/patches/lora_conversions/anima_lora_constants.py new file mode 100644 index 00000000000..380e31998a7 --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/anima_lora_constants.py @@ -0,0 +1,45 @@ +# Anima LoRA prefix constants +# These prefixes are used for key mapping when applying LoRA patches to Anima models + +import re + +# Prefix for Anima transformer (Cosmos DiT architecture) LoRA layers +ANIMA_LORA_TRANSFORMER_PREFIX = "lora_transformer-" + +# Prefix for Qwen3 text encoder LoRA layers +ANIMA_LORA_QWEN3_PREFIX = "lora_qwen3-" + +# --------------------------------------------------------------------------- +# Cosmos DiT detection helpers +# +# Shared between ``anima_lora_conversion_utils.is_state_dict_likely_anima_lora`` +# and the config probing code in ``configs/lora.py``. Kept here (rather than +# in ``anima_lora_conversion_utils``) to avoid circular imports. +# --------------------------------------------------------------------------- + +# Cosmos DiT subcomponent names unique to the Anima / Cosmos Predict2 architecture. +_COSMOS_DIT_SUBCOMPONENTS_RE = r"(cross_attn|self_attn|mlp|adaln_modulation)" + +# Kohya format: lora_unet_[llm_adapter_]blocks_N_ +_KOHYA_ANIMA_RE = re.compile(r"lora_unet_(llm_adapter_)?blocks_\d+_" + _COSMOS_DIT_SUBCOMPONENTS_RE) + +# PEFT format: .blocks.N. +_PEFT_ANIMA_RE = re.compile( + r"(diffusion_model|transformer|base_model\.model\.transformer)\.blocks\.\d+\." + _COSMOS_DIT_SUBCOMPONENTS_RE +) + + +def has_cosmos_dit_kohya_keys(str_keys: list[str]) -> bool: + """Check for Kohya-style keys targeting Cosmos DiT blocks with specific subcomponents. + + Requires both the ``lora_unet_[llm_adapter_]blocks_N_`` prefix **and** a + Cosmos DiT subcomponent name (cross_attn, self_attn, mlp, adaln_modulation) + to avoid false-positives on other architectures that might also use bare + ``blocks`` in their key paths. + """ + return any(_KOHYA_ANIMA_RE.search(k) is not None for k in str_keys) + + +def has_cosmos_dit_peft_keys(str_keys: list[str]) -> bool: + """Check for diffusers PEFT keys targeting Cosmos DiT blocks with specific subcomponents.""" + return any(_PEFT_ANIMA_RE.search(k) is not None for k in str_keys) diff --git a/invokeai/backend/patches/lora_conversions/anima_lora_conversion_utils.py b/invokeai/backend/patches/lora_conversions/anima_lora_conversion_utils.py new file mode 100644 index 00000000000..b55a96dca75 --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/anima_lora_conversion_utils.py @@ -0,0 +1,300 @@ +"""Anima LoRA conversion utilities. + +Anima uses a Cosmos Predict2 DiT transformer architecture. +LoRAs for Anima typically follow the Kohya-style format with underscore-separated keys +(e.g., lora_unet_blocks_0_cross_attn_k_proj) that map to model parameter paths +(e.g., blocks.0.cross_attn.k_proj). + +Some Anima LoRAs also target the Qwen3 text encoder with lora_te_ prefix keys +(e.g., lora_te_layers_0_self_attn_q_proj -> layers.0.self_attn.q_proj). +""" + +import re +from typing import Dict + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.utils import any_lora_layer_from_state_dict +from invokeai.backend.patches.lora_conversions.anima_lora_constants import ( + ANIMA_LORA_QWEN3_PREFIX, + ANIMA_LORA_TRANSFORMER_PREFIX, + has_cosmos_dit_kohya_keys, + has_cosmos_dit_peft_keys, +) +from invokeai.backend.patches.model_patch_raw import ModelPatchRaw +from invokeai.backend.util.logging import InvokeAILogger + +logger = InvokeAILogger.get_logger(__name__) + + +def is_state_dict_likely_anima_lora(state_dict: dict[str | int, torch.Tensor]) -> bool: + """Checks if the provided state dict is likely an Anima LoRA. + + Anima LoRAs use Kohya-style naming with lora_unet_ prefix and underscore-separated + model key paths targeting Cosmos DiT blocks. Detection requires Cosmos DiT-specific + subcomponent names (cross_attn, self_attn, mlp, adaln_modulation) to avoid + false-positives on other architectures that also use ``blocks`` in their paths. + """ + str_keys = [k for k in state_dict.keys() if isinstance(k, str)] + + if has_cosmos_dit_kohya_keys(str_keys): + return True + + return has_cosmos_dit_peft_keys(str_keys) + + +# Mapping from Kohya underscore-style substrings to model parameter names. +# Order matters: longer/more specific patterns should come first to avoid partial matches. +_KOHYA_UNET_KEY_REPLACEMENTS = [ + ("adaln_modulation_cross_attn_", "adaln_modulation_cross_attn."), + ("adaln_modulation_self_attn_", "adaln_modulation_self_attn."), + ("adaln_modulation_mlp_", "adaln_modulation_mlp."), + ("cross_attn_k_proj", "cross_attn.k_proj"), + ("cross_attn_q_proj", "cross_attn.q_proj"), + ("cross_attn_v_proj", "cross_attn.v_proj"), + ("cross_attn_output_proj", "cross_attn.output_proj"), + ("cross_attn_o_proj", "cross_attn.o_proj"), + ("self_attn_k_proj", "self_attn.k_proj"), + ("self_attn_q_proj", "self_attn.q_proj"), + ("self_attn_v_proj", "self_attn.v_proj"), + ("self_attn_output_proj", "self_attn.output_proj"), + ("self_attn_o_proj", "self_attn.o_proj"), + ("mlp_layer1", "mlp.layer1"), + ("mlp_layer2", "mlp.layer2"), +] + +# Mapping for Qwen3 text encoder Kohya keys. +_KOHYA_TE_KEY_REPLACEMENTS = [ + ("self_attn_k_proj", "self_attn.k_proj"), + ("self_attn_q_proj", "self_attn.q_proj"), + ("self_attn_v_proj", "self_attn.v_proj"), + ("self_attn_o_proj", "self_attn.o_proj"), + ("mlp_down_proj", "mlp.down_proj"), + ("mlp_gate_proj", "mlp.gate_proj"), + ("mlp_up_proj", "mlp.up_proj"), +] + + +def _convert_kohya_unet_key(kohya_layer_name: str) -> str: + """Convert a Kohya-style LoRA layer name to a model parameter path. + + Example: lora_unet_blocks_0_cross_attn_k_proj -> blocks.0.cross_attn.k_proj + Example: lora_unet_llm_adapter_blocks_0_cross_attn_k_proj -> llm_adapter.blocks.0.cross_attn.k_proj + """ + key = kohya_layer_name + if key.startswith("lora_unet_"): + key = key[len("lora_unet_") :] + + # Handle llm_adapter prefix: strip it, run the standard block conversion, then re-add with dot + llm_adapter_prefix = "" + if key.startswith("llm_adapter_"): + key = key[len("llm_adapter_") :] + llm_adapter_prefix = "llm_adapter." + + # Convert blocks_N_ to blocks.N. + key = re.sub(r"^blocks_(\d+)_", r"blocks.\1.", key) + + # Apply known replacements for subcomponent names + for old, new in _KOHYA_UNET_KEY_REPLACEMENTS: + if old in key: + key = key.replace(old, new, 1) + break + + return llm_adapter_prefix + key + + +def _convert_kohya_te_key(kohya_layer_name: str) -> str: + """Convert a Kohya-style text encoder LoRA layer name to a model parameter path. + + The Qwen3 text encoder is loaded as Qwen3ForCausalLM which wraps the base model + under a `model.` prefix, so the final path must include it. + + Example: lora_te_layers_0_self_attn_q_proj -> model.layers.0.self_attn.q_proj + """ + key = kohya_layer_name + if key.startswith("lora_te_"): + key = key[len("lora_te_") :] + + # Convert layers_N_ to layers.N. + key = re.sub(r"^layers_(\d+)_", r"layers.\1.", key) + + # Apply known replacements + for old, new in _KOHYA_TE_KEY_REPLACEMENTS: + if old in key: + key = key.replace(old, new, 1) + break + + # Qwen3ForCausalLM wraps the base Qwen3Model under `model.` + key = f"model.{key}" + + return key + + +def _make_layer_patch(layer_dict: dict[str, torch.Tensor]) -> BaseLayerPatch: + """Create a layer patch from a layer dict, handling DoRA+LoKR edge case. + + Some Anima LoRAs combine DoRA (dora_scale) with LoKR (lokr_w1/lokr_w2) weights. + The shared any_lora_layer_from_state_dict checks dora_scale first and expects + lora_up/lora_down keys, which don't exist in LoKR layers. We strip dora_scale + from LoKR layers so they fall through to the LoKR handler instead. + """ + has_lokr = "lokr_w1" in layer_dict or "lokr_w1_a" in layer_dict + has_dora = "dora_scale" in layer_dict + if has_lokr and has_dora: + layer_dict = {k: v for k, v in layer_dict.items() if k != "dora_scale"} + logger.warning("Stripped dora_scale from LoKR layer (DoRA+LoKR combination not supported, using LoKR only)") + return any_lora_layer_from_state_dict(layer_dict) + + +# Known suffixes for Kohya format +_KOHYA_KNOWN_SUFFIXES = [ + ".lora_A.weight", + ".lora_B.weight", + ".lora_down.weight", + ".lora_up.weight", + ".dora_scale", + ".alpha", +] + +# Additional suffixes for PEFT/LoKR format +_PEFT_EXTRA_SUFFIXES = [ + ".lokr_w1", + ".lokr_w2", + ".lokr_w1_a", + ".lokr_w1_b", + ".lokr_w2_a", + ".lokr_w2_b", +] + + +def _group_keys_by_layer( + state_dict: Dict[str, torch.Tensor], + extra_suffixes: list[str] | None = None, +) -> dict[str, dict[str, torch.Tensor]]: + """Group state dict keys by layer name based on known suffixes. + + Args: + state_dict: The LoRA state dict to group. + extra_suffixes: Additional suffixes to recognize beyond the base Kohya set. + + Returns: + Dict mapping layer names to their component tensors. + """ + layer_dict: dict[str, dict[str, torch.Tensor]] = {} + + known_suffixes = list(_KOHYA_KNOWN_SUFFIXES) + if extra_suffixes: + known_suffixes.extend(extra_suffixes) + + for key in state_dict: + if not isinstance(key, str): + continue + + layer_name = None + key_name = None + for suffix in known_suffixes: + if key.endswith(suffix): + layer_name = key[: -len(suffix)] + key_name = suffix[1:] # Remove leading dot + break + + if layer_name is None: + parts = key.rsplit(".", maxsplit=2) + layer_name = parts[0] + key_name = ".".join(parts[1:]) + + if layer_name not in layer_dict: + layer_dict[layer_name] = {} + layer_dict[layer_name][key_name] = state_dict[key] + + return layer_dict + + +def _get_lora_layer_values(layer_dict: dict[str, torch.Tensor], alpha: float | None) -> dict[str, torch.Tensor]: + """Convert layer dict keys from PEFT format to internal format.""" + if "lora_A.weight" in layer_dict: + values = { + "lora_down.weight": layer_dict["lora_A.weight"], + "lora_up.weight": layer_dict["lora_B.weight"], + } + if alpha is not None: + values["alpha"] = torch.tensor(alpha) + return values + elif "lora_down.weight" in layer_dict: + return layer_dict + else: + return layer_dict + + +def lora_model_from_anima_state_dict(state_dict: Dict[str, torch.Tensor], alpha: float | None = None) -> ModelPatchRaw: + """Convert an Anima LoRA state dict to a ModelPatchRaw. + + Supports both Kohya-style keys (lora_unet_blocks_0_...) and diffusers PEFT format. + Also supports text encoder LoRA keys (lora_te_layers_0_...) targeting the Qwen3 encoder. + + Args: + state_dict: The LoRA state dict + alpha: The alpha value for LoRA scaling. If None, uses rank as alpha. + + Returns: + A ModelPatchRaw containing the LoRA layers + """ + layers: dict[str, BaseLayerPatch] = {} + + # Detect format + str_keys = [k for k in state_dict.keys() if isinstance(k, str)] + is_kohya = any(k.startswith(("lora_unet_", "lora_te_")) for k in str_keys) + + if is_kohya: + # Kohya format: group by layer name (everything before .lora_down/.lora_up/.alpha) + grouped = _group_keys_by_layer(state_dict) + for kohya_layer_name, layer_dict in grouped.items(): + if kohya_layer_name.startswith("lora_te_"): + model_key = _convert_kohya_te_key(kohya_layer_name) + final_key = f"{ANIMA_LORA_QWEN3_PREFIX}{model_key}" + else: + model_key = _convert_kohya_unet_key(kohya_layer_name) + final_key = f"{ANIMA_LORA_TRANSFORMER_PREFIX}{model_key}" + layer = _make_layer_patch(layer_dict) + layers[final_key] = layer + else: + # Diffusers PEFT format + grouped = _group_keys_by_layer(state_dict, extra_suffixes=_PEFT_EXTRA_SUFFIXES) + for layer_key, layer_dict in grouped.items(): + values = _get_lora_layer_values(layer_dict, alpha) + clean_key = layer_key + + # Check for text encoder prefixes + text_encoder_prefixes = [ + "base_model.model.text_encoder.", + "text_encoder.", + ] + + is_text_encoder = False + for prefix in text_encoder_prefixes: + if layer_key.startswith(prefix): + clean_key = layer_key[len(prefix) :] + is_text_encoder = True + break + + # If not text encoder, check transformer prefixes + if not is_text_encoder: + for prefix in [ + "base_model.model.transformer.", + "transformer.", + "diffusion_model.", + ]: + if layer_key.startswith(prefix): + clean_key = layer_key[len(prefix) :] + break + + if is_text_encoder: + final_key = f"{ANIMA_LORA_QWEN3_PREFIX}{clean_key}" + else: + final_key = f"{ANIMA_LORA_TRANSFORMER_PREFIX}{clean_key}" + + layer = _make_layer_patch(values) + layers[final_key] = layer + + return ModelPatchRaw(layers=layers) diff --git a/invokeai/backend/patches/lora_conversions/flux_aitoolkit_lora_conversion_utils.py b/invokeai/backend/patches/lora_conversions/flux_aitoolkit_lora_conversion_utils.py new file mode 100644 index 00000000000..f359e7caa32 --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/flux_aitoolkit_lora_conversion_utils.py @@ -0,0 +1,102 @@ +import json +from dataclasses import dataclass, field +from typing import Any + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.utils import any_lora_layer_from_state_dict +from invokeai.backend.patches.lora_conversions.flux_diffusers_lora_conversion_utils import _group_by_layer +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.util import InvokeAILogger + + +def _has_flux_layer_structure(state_dict: dict[str | int, Any]) -> bool: + """Check if state dict has Flux-specific layer patterns (double_blocks/single_blocks).""" + return any( + k.startswith("diffusion_model.double_blocks.") or k.startswith("diffusion_model.single_blocks.") + for k in state_dict.keys() + if isinstance(k, str) + ) + + +def is_state_dict_likely_in_flux_aitoolkit_format( + state_dict: dict[str | int, Any], + metadata: dict[str, Any] | None = None, +) -> bool: + # Always check for Flux-specific layer structure first + # This prevents misidentifying Z-Image LoRAs (which use diffusion_model.layers.X) as Flux + if not _has_flux_layer_structure(state_dict): + return False + + # AIToolkit only produces standard PEFT LoRA (lora_A.weight / lora_B.weight). + # Exclude LyCORIS algorithm variants (LoKR, LoHA, etc.) which use different weight key suffixes. + # These are handled by the BFL PEFT converter instead. + _LYCORIS_SUFFIXES = ( + "lokr_w1", + "lokr_w2", + "lokr_w1_a", + "lokr_w1_b", + "lokr_w2_a", + "lokr_w2_b", + "lokr_t2", + "hada_w1_a", + "hada_w1_b", + "hada_w2_a", + "hada_w2_b", + "hada_t1", + "hada_t2", + ) + if any(k.endswith(_LYCORIS_SUFFIXES) for k in state_dict.keys() if isinstance(k, str)): + return False + + if metadata: + try: + software = json.loads(metadata.get("software", "{}")) + except json.JSONDecodeError: + return False + return software.get("name") == "ai-toolkit" + + # No metadata - if it has Flux layer structure, assume it's AI Toolkit format + return True + + +@dataclass +class GroupedStateDict: + transformer: dict[str, Any] = field(default_factory=dict) + # might also grow CLIP and T5 submodels + + +def _group_state_by_submodel(state_dict: dict[str, Any]) -> GroupedStateDict: + logger = InvokeAILogger.get_logger() + grouped = GroupedStateDict() + for key, value in state_dict.items(): + submodel_name, param_name = key.split(".", 1) + match submodel_name: + case "diffusion_model": + grouped.transformer[param_name] = value + case _: + logger.warning(f"Unexpected submodel name: {submodel_name}") + return grouped + + +def _rename_peft_lora_keys(state_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: + """Renames keys from the PEFT LoRA format to the InvokeAI format.""" + renamed_state_dict = {} + for key, value in state_dict.items(): + renamed_key = key.replace(".lora_A.", ".lora_down.").replace(".lora_B.", ".lora_up.") + renamed_state_dict[renamed_key] = value + return renamed_state_dict + + +def lora_model_from_flux_aitoolkit_state_dict(state_dict: dict[str, torch.Tensor]) -> ModelPatchRaw: + state_dict = _rename_peft_lora_keys(state_dict) + by_layer = _group_by_layer(state_dict) + by_model = _group_state_by_submodel(by_layer) + + layers: dict[str, BaseLayerPatch] = {} + for layer_key, layer_state_dict in by_model.transformer.items(): + layers[FLUX_LORA_TRANSFORMER_PREFIX + layer_key] = any_lora_layer_from_state_dict(layer_state_dict) + + return ModelPatchRaw(layers=layers) diff --git a/invokeai/backend/patches/lora_conversions/flux_bfl_peft_lora_conversion_utils.py b/invokeai/backend/patches/lora_conversions/flux_bfl_peft_lora_conversion_utils.py new file mode 100644 index 00000000000..fd89d673c8f --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/flux_bfl_peft_lora_conversion_utils.py @@ -0,0 +1,539 @@ +"""Utilities for detecting and converting FLUX LoRAs in BFL PEFT format. + +This format uses BFL internal key names (double_blocks, single_blocks, etc.) with a +'diffusion_model.' prefix and PEFT-style LoRA suffixes (lora_A.weight, lora_B.weight). +LyCORIS variants (LoKR, LoHA, etc.) are also supported, using their respective weight key +suffixes (lokr_w1, lokr_w2, hada_w1_a, etc.) in place of the PEFT suffixes. + +Example keys (LoRA PEFT): + diffusion_model.double_blocks.0.img_attn.proj.lora_A.weight + diffusion_model.double_blocks.0.img_attn.qkv.lora_B.weight + diffusion_model.single_blocks.0.linear1.lora_A.weight + +Example keys (LoKR): + diffusion_model.double_blocks.0.img_attn.proj.lokr_w1 + diffusion_model.double_blocks.0.img_attn.proj.lokr_w2 + diffusion_model.single_blocks.0.linear1.lokr_w1 + +This format is used by some training tools (e.g. SimpleTuner, ComfyUI-based trainers) +and is common for FLUX.2 Klein LoRAs. +""" + +import re +from typing import Dict + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.lora_layer import LoRALayer +from invokeai.backend.patches.layers.utils import any_lora_layer_from_state_dict +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.util.logging import InvokeAILogger + +logger = InvokeAILogger.get_logger(__name__) + +# The prefixes used in BFL PEFT format LoRAs. +# Most commonly "diffusion_model.", but some PEFT-wrapped variants use "base_model.model.". +_BFL_PEFT_PREFIX = "diffusion_model." +_PEFT_BASE_MODEL_PREFIX = "base_model.model." +_BFL_PEFT_PREFIXES = (_BFL_PEFT_PREFIX, _PEFT_BASE_MODEL_PREFIX) + +# Key patterns that identify FLUX architecture in BFL format +_BFL_FLUX_BLOCK_PREFIXES = ( + f"{_BFL_PEFT_PREFIX}double_blocks.", + f"{_BFL_PEFT_PREFIX}single_blocks.", + f"{_PEFT_BASE_MODEL_PREFIX}double_blocks.", + f"{_PEFT_BASE_MODEL_PREFIX}single_blocks.", +) + +# Regex patterns for converting BFL layer names to diffusers naming (for FLUX.2 Klein). +# BFL uses fused QKV, diffusers uses separate Q/K/V for double blocks. +_DOUBLE_BLOCK_RE = re.compile(r"^double_blocks\.(\d+)\.(.+)$") +_SINGLE_BLOCK_RE = re.compile(r"^single_blocks\.(\d+)\.(.+)$") + +# Weight key suffixes used by PEFT LoRA in BFL format. +_BFL_PEFT_LORA_SUFFIXES = ("lora_A.weight", "lora_B.weight") + +# Weight key suffixes used by LyCORIS algorithms (LoKR, LoHA, etc.) in BFL format. +# These are single-component suffixes (no dot), unlike the two-component PEFT suffixes. +_BFL_LYCORIS_WEIGHT_SUFFIXES = ( + # LoKR + "lokr_w1", + "lokr_w2", + "lokr_w1_a", + "lokr_w1_b", + "lokr_w2_a", + "lokr_w2_b", + "lokr_t2", + # LoHA + "hada_w1_a", + "hada_w1_b", + "hada_w2_a", + "hada_w2_b", + "hada_t1", + "hada_t2", + # Common to all LyCORIS algorithms + "alpha", + "dora_scale", + # Full/Diff + "diff", +) + +# All recognized BFL weight key suffixes (both PEFT and LyCORIS). +_BFL_ALL_WEIGHT_SUFFIXES = _BFL_PEFT_LORA_SUFFIXES + _BFL_LYCORIS_WEIGHT_SUFFIXES + +# Mapping of BFL double block layer suffixes to diffusers equivalents (simple renames). +_DOUBLE_BLOCK_RENAMES: dict[str, str] = { + "img_attn.proj": "attn.to_out.0", + "txt_attn.proj": "attn.to_add_out", + "img_mlp.0": "ff.linear_in", + "img_mlp.2": "ff.linear_out", + "txt_mlp.0": "ff_context.linear_in", + "txt_mlp.2": "ff_context.linear_out", +} + +# Mapping of BFL single block layer suffixes to diffusers equivalents. +_SINGLE_BLOCK_RENAMES: dict[str, str] = { + "linear1": "attn.to_qkv_mlp_proj", + "linear2": "attn.to_out", +} + +# Mapping of BFL non-block layer names to diffusers equivalents. +# These are top-level modules (embedders, modulations, output layers) that use different +# names in BFL's FLUX.2 model vs the diffusers Flux2Transformer2DModel. +_NON_BLOCK_RENAMES: dict[str, str] = { + "img_in": "x_embedder", + "txt_in": "context_embedder", + "double_stream_modulation_img.lin": "double_stream_modulation_img.linear", + "double_stream_modulation_txt.lin": "double_stream_modulation_txt.linear", + "single_stream_modulation.lin": "single_stream_modulation.linear", + "final_layer.linear": "proj_out", + "time_in.in_layer": "time_guidance_embed.timestep_embedder.linear_1", + "time_in.out_layer": "time_guidance_embed.timestep_embedder.linear_2", +} + + +def is_state_dict_likely_in_flux_bfl_peft_format(state_dict: dict[str | int, torch.Tensor]) -> bool: + """Checks if the provided state dict is likely in the BFL PEFT FLUX LoRA/LyCORIS format. + + This format uses BFL key names (double_blocks, single_blocks, img_attn, etc.) with either + PEFT LoRA suffixes (lora_A.weight, lora_B.weight) or LyCORIS algorithm suffixes (lokr_w1, + lokr_w2, hada_w1_a, etc.). The keys may be prefixed with either 'diffusion_model.' + (common for ComfyUI/SimpleTuner) or 'base_model.model.' (PEFT-wrapped variant). + """ + str_keys = [k for k in state_dict.keys() if isinstance(k, str)] + if not str_keys: + return False + + # All keys must use recognized weight suffixes (PEFT LoRA or LyCORIS). + all_valid_suffixes = all(k.endswith(_BFL_ALL_WEIGHT_SUFFIXES) for k in str_keys) + if not all_valid_suffixes: + return False + + # Must have at least some keys with FLUX block structure (double_blocks/single_blocks) + has_flux_blocks = any(k.startswith(_BFL_FLUX_BLOCK_PREFIXES) for k in str_keys) + if not has_flux_blocks: + return False + + # All keys should share the same recognized prefix + all_have_prefix = all(k.startswith(_BFL_PEFT_PREFIXES) for k in str_keys) + + return all_have_prefix + + +def _strip_bfl_peft_prefix(key: str) -> str: + """Strip the BFL PEFT prefix ('diffusion_model.' or 'base_model.model.') from a key.""" + for prefix in _BFL_PEFT_PREFIXES: + if key.startswith(prefix): + return key[len(prefix) :] + return key + + +def _split_bfl_key(key: str) -> tuple[str, str]: + """Split a BFL key (after prefix stripping) into (layer_name, weight_suffix). + + Handles: + - 2-component suffixes ending with '.weight': e.g., 'lora_A.weight', 'lora_B.weight' + - 1-component suffixes: e.g., 'lokr_w1', 'lokr_w2', 'alpha', 'dora_scale' + """ + if key.endswith(".weight"): + # 2-component suffix: e.g., 'lora_A.weight' → split at last 2 dots + parts = key.rsplit(".", maxsplit=2) + return parts[0], f"{parts[1]}.{parts[2]}" + else: + # 1-component suffix: e.g., 'lokr_w1', 'alpha' → split at last dot + parts = key.rsplit(".", maxsplit=1) + return parts[0], parts[1] + + +def lora_model_from_flux_bfl_peft_state_dict( + state_dict: Dict[str, torch.Tensor], alpha: float | None = None +) -> ModelPatchRaw: + """Convert a BFL PEFT/LyCORIS format FLUX LoRA state dict to a ModelPatchRaw. + + The conversion is straightforward: strip the prefix ('diffusion_model.' or 'base_model.model.') + to get the BFL internal key names, which are already the format used by InvokeAI internally. + Supports both PEFT LoRA (lora_A.weight / lora_B.weight) and LyCORIS algorithms (LoKR, LoHA, etc.). + """ + # Group keys by layer + grouped_state_dict: dict[str, dict[str, torch.Tensor]] = {} + for key, value in state_dict.items(): + # Strip the prefix + if isinstance(key, str): + key = _strip_bfl_peft_prefix(key) + + layer_name, suffix = _split_bfl_key(key) + + if layer_name not in grouped_state_dict: + grouped_state_dict[layer_name] = {} + + # Convert PEFT naming to InvokeAI naming; LyCORIS keys pass through unchanged. + if suffix == "lora_A.weight": + grouped_state_dict[layer_name]["lora_down.weight"] = value + elif suffix == "lora_B.weight": + grouped_state_dict[layer_name]["lora_up.weight"] = value + else: + grouped_state_dict[layer_name][suffix] = value + + # Add alpha if provided + if alpha is not None: + for layer_state_dict in grouped_state_dict.values(): + layer_state_dict["alpha"] = torch.tensor(alpha) + + # Build LoRA layers with the transformer prefix + layers = {} + for layer_key, layer_state_dict in grouped_state_dict.items(): + layers[f"{FLUX_LORA_TRANSFORMER_PREFIX}{layer_key}"] = any_lora_layer_from_state_dict(layer_state_dict) + + return ModelPatchRaw(layers=layers) + + +def lora_model_from_flux2_bfl_peft_state_dict( + state_dict: Dict[str, torch.Tensor], alpha: float | None = None +) -> ModelPatchRaw: + """Convert a BFL PEFT/LyCORIS format FLUX LoRA state dict for use with FLUX.2 Klein (diffusers model). + + FLUX.2 Klein models are loaded as Flux2Transformer2DModel (diffusers), which uses different + layer naming than BFL's internal format: + - double_blocks.{i} → transformer_blocks.{i} + - single_blocks.{i} → single_transformer_blocks.{i} + - Fused QKV (img_attn.qkv) → separate Q/K/V (attn.to_q, attn.to_k, attn.to_v) + + This function converts BFL PEFT/LyCORIS keys to diffusers naming and splits fused QKV LoRAs + into separate Q/K/V LoRA layers. + """ + # First, strip the prefix and group by BFL layer name with PEFT→InvokeAI naming. + grouped_state_dict: dict[str, dict[str, torch.Tensor]] = {} + for key, value in state_dict.items(): + if isinstance(key, str): + key = _strip_bfl_peft_prefix(key) + + layer_name, suffix = _split_bfl_key(key) + + if layer_name not in grouped_state_dict: + grouped_state_dict[layer_name] = {} + + if suffix == "lora_A.weight": + grouped_state_dict[layer_name]["lora_down.weight"] = value + elif suffix == "lora_B.weight": + grouped_state_dict[layer_name]["lora_up.weight"] = value + else: + grouped_state_dict[layer_name][suffix] = value + + if alpha is not None: + for layer_state_dict in grouped_state_dict.values(): + layer_state_dict["alpha"] = torch.tensor(alpha) + + # Now convert BFL layer names to diffusers naming, splitting fused QKV as needed. + layers: dict[str, any] = {} + for bfl_key, layer_sd in grouped_state_dict.items(): + diffusers_layers = _convert_bfl_layer_to_diffusers(bfl_key, layer_sd) + for diff_key, diff_sd in diffusers_layers: + layers[f"{FLUX_LORA_TRANSFORMER_PREFIX}{diff_key}"] = any_lora_layer_from_state_dict(diff_sd) + + return ModelPatchRaw(layers=layers) + + +def _convert_bfl_layer_to_diffusers( + bfl_key: str, layer_sd: dict[str, torch.Tensor] +) -> list[tuple[str, dict[str, torch.Tensor]]]: + """Convert a single BFL-named LoRA/LyCORIS layer to one or more diffusers-named layers. + + Returns a list of (diffusers_key, layer_state_dict) tuples. Most layers produce one entry, + but fused QKV layers are split into three separate Q/K/V entries. + """ + # Double blocks + m = _DOUBLE_BLOCK_RE.match(bfl_key) + if m: + idx, rest = m.group(1), m.group(2) + prefix = f"transformer_blocks.{idx}" + + # Fused image QKV → split into separate Q, K, V + if rest == "img_attn.qkv": + if "lora_down.weight" in layer_sd: + return _split_qkv_lora( + layer_sd, + q_key=f"{prefix}.attn.to_q", + k_key=f"{prefix}.attn.to_k", + v_key=f"{prefix}.attn.to_v", + ) + elif "lokr_w1" in layer_sd or "lokr_w1_a" in layer_sd: + return _split_qkv_lokr( + layer_sd, + q_key=f"{prefix}.attn.to_q", + k_key=f"{prefix}.attn.to_k", + v_key=f"{prefix}.attn.to_v", + ) + else: + logger.warning(f"Unsupported layer type for QKV split in {bfl_key}; layer will be skipped.") + return [] + # Fused text QKV → split into separate Q, K, V + if rest == "txt_attn.qkv": + if "lora_down.weight" in layer_sd: + return _split_qkv_lora( + layer_sd, + q_key=f"{prefix}.attn.add_q_proj", + k_key=f"{prefix}.attn.add_k_proj", + v_key=f"{prefix}.attn.add_v_proj", + ) + elif "lokr_w1" in layer_sd or "lokr_w1_a" in layer_sd: + return _split_qkv_lokr( + layer_sd, + q_key=f"{prefix}.attn.add_q_proj", + k_key=f"{prefix}.attn.add_k_proj", + v_key=f"{prefix}.attn.add_v_proj", + ) + else: + logger.warning(f"Unsupported layer type for QKV split in {bfl_key}; layer will be skipped.") + return [] + # Simple renames + if rest in _DOUBLE_BLOCK_RENAMES: + return [(f"{prefix}.{_DOUBLE_BLOCK_RENAMES[rest]}", layer_sd)] + + # Fallback: keep as-is under the new prefix + return [(f"{prefix}.{rest}", layer_sd)] + + # Single blocks + m = _SINGLE_BLOCK_RE.match(bfl_key) + if m: + idx, rest = m.group(1), m.group(2) + prefix = f"single_transformer_blocks.{idx}" + + if rest in _SINGLE_BLOCK_RENAMES: + return [(f"{prefix}.{_SINGLE_BLOCK_RENAMES[rest]}", layer_sd)] + + return [(f"{prefix}.{rest}", layer_sd)] + + # Non-block keys (embedders, modulations, output layers) + if bfl_key in _NON_BLOCK_RENAMES: + return [(_NON_BLOCK_RENAMES[bfl_key], layer_sd)] + + # Fallback: pass through unchanged + return [(bfl_key, layer_sd)] + + +def _split_qkv_lora( + layer_sd: dict[str, torch.Tensor], + q_key: str, + k_key: str, + v_key: str, +) -> list[tuple[str, dict[str, torch.Tensor]]]: + """Split a fused QKV LoRA layer into separate Q, K, V LoRA layers. + + BFL uses fused QKV: lora_down [rank, hidden], lora_up [3*hidden, rank]. + Diffusers uses separate layers: each gets lora_down (shared/cloned) and a third of lora_up. + """ + lora_down = layer_sd["lora_down.weight"] # [rank, hidden] + lora_up = layer_sd["lora_up.weight"] # [3*hidden, rank] + alpha = layer_sd.get("alpha") + + # Split lora_up into 3 equal parts along dim 0 + up_q, up_k, up_v = lora_up.chunk(3, dim=0) + + result = [] + for key, up_part in [(q_key, up_q), (k_key, up_k), (v_key, up_v)]: + sd: dict[str, torch.Tensor] = { + "lora_down.weight": lora_down.clone(), + "lora_up.weight": up_part, + } + if alpha is not None: + sd["alpha"] = alpha + result.append((key, sd)) + + return result + + +def _split_qkv_lokr( + layer_sd: dict[str, torch.Tensor], + q_key: str, + k_key: str, + v_key: str, +) -> list[tuple[str, dict[str, torch.Tensor]]]: + """Split a fused QKV LoKR layer into separate Q, K, V full (diff) layers. + + LoKR uses a Kronecker product which cannot be split cleanly, so we compute the full weight + matrix and store each third as a full weight update (diff). + + BFL uses fused QKV: full weight [3*hidden, hidden]. + Diffusers uses separate layers: each gets a [hidden, hidden] weight slice. + + For factorized LOKR (w1_a/w1_b), the alpha/rank scale is baked into the diff weights because + FullLayer always uses scale=1.0. + """ + w1 = layer_sd.get("lokr_w1") + w1_a = layer_sd.get("lokr_w1_a") + w1_b = layer_sd.get("lokr_w1_b") + w2 = layer_sd.get("lokr_w2") + w2_a = layer_sd.get("lokr_w2_a") + w2_b = layer_sd.get("lokr_w2_b") + t2 = layer_sd.get("lokr_t2") + alpha = layer_sd.get("alpha") + + # Compute rank for scaling (only valid for factorized LOKR). + if w1_b is not None: + rank: int | None = w1_b.shape[0] + elif w2_b is not None: + rank = w2_b.shape[0] + else: + rank = None + + if w1 is None: + assert w1_a is not None and w1_b is not None + w1 = w1_a @ w1_b + if w2 is None: + assert w2_a is not None and w2_b is not None + if t2 is not None: + w2 = torch.einsum("i j k l, i p, j r -> p r k l", t2, w2_a, w2_b) + else: + w2 = w2_a @ w2_b + + if len(w2.shape) == 4: + w1 = w1.unsqueeze(2).unsqueeze(2) + + full_weight = torch.kron(w1, w2) # [3*hidden, hidden] + + # For factorized LOKR, bake the alpha/rank scale into the weight because FullLayer.scale() + # always returns 1.0 (it has no alpha). For non-factorized LOKR, rank is None and scale is 1.0. + if rank is not None and alpha is not None: + scale = alpha.item() / rank + full_weight = full_weight * scale + + weight_q, weight_k, weight_v = full_weight.chunk(3, dim=0) + + result = [] + for key, weight_part in [(q_key, weight_q), (k_key, weight_k), (v_key, weight_v)]: + result.append((key, {"diff": weight_part})) + + return result + + +def convert_bfl_lora_patch_to_diffusers(patch: ModelPatchRaw) -> ModelPatchRaw: + """Convert a ModelPatchRaw with BFL-format layer keys to diffusers-format keys. + + This handles LoRAs that were loaded with the FLUX.1 BFL PEFT converter (which keeps BFL keys) + but need to be applied to a FLUX.2 Klein model (which uses diffusers module names). + + If the patch doesn't contain BFL-format keys, it is returned unchanged. + """ + prefix = FLUX_LORA_TRANSFORMER_PREFIX + prefix_len = len(prefix) + + # Check if any layer keys are in BFL format (contain double_blocks or single_blocks) + has_bfl_keys = any( + k.startswith(prefix) + and (k[prefix_len:].startswith("double_blocks.") or k[prefix_len:].startswith("single_blocks.")) + for k in patch.layers + ) + if not has_bfl_keys: + return patch + + new_layers: dict[str, BaseLayerPatch] = {} + for layer_key, layer in patch.layers.items(): + if not layer_key.startswith(prefix): + new_layers[layer_key] = layer + continue + + bfl_key = layer_key[prefix_len:] + converted = _convert_bfl_layer_patch_to_diffusers(bfl_key, layer) + for diff_key, diff_layer in converted: + new_layers[f"{prefix}{diff_key}"] = diff_layer + + return ModelPatchRaw(layers=new_layers) + + +def _convert_bfl_layer_patch_to_diffusers(bfl_key: str, layer: BaseLayerPatch) -> list[tuple[str, BaseLayerPatch]]: + """Convert a single BFL-named LoRA layer patch to one or more diffusers-named patches. + + For simple renames, the layer object is reused. For QKV splits, new LoRALayer objects + are created with split up-weights and cloned down-weights. + """ + # Double blocks + m = _DOUBLE_BLOCK_RE.match(bfl_key) + if m: + idx, rest = m.group(1), m.group(2) + diff_prefix = f"transformer_blocks.{idx}" + + # Fused QKV → split into separate Q, K, V + if rest == "img_attn.qkv" and isinstance(layer, LoRALayer): + return _split_qkv_lora_layer( + layer, + q_key=f"{diff_prefix}.attn.to_q", + k_key=f"{diff_prefix}.attn.to_k", + v_key=f"{diff_prefix}.attn.to_v", + ) + if rest == "txt_attn.qkv" and isinstance(layer, LoRALayer): + return _split_qkv_lora_layer( + layer, + q_key=f"{diff_prefix}.attn.add_q_proj", + k_key=f"{diff_prefix}.attn.add_k_proj", + v_key=f"{diff_prefix}.attn.add_v_proj", + ) + # Simple renames + if rest in _DOUBLE_BLOCK_RENAMES: + return [(f"{diff_prefix}.{_DOUBLE_BLOCK_RENAMES[rest]}", layer)] + return [(f"{diff_prefix}.{rest}", layer)] + + # Single blocks + m = _SINGLE_BLOCK_RE.match(bfl_key) + if m: + idx, rest = m.group(1), m.group(2) + diff_prefix = f"single_transformer_blocks.{idx}" + + if rest in _SINGLE_BLOCK_RENAMES: + return [(f"{diff_prefix}.{_SINGLE_BLOCK_RENAMES[rest]}", layer)] + return [(f"{diff_prefix}.{rest}", layer)] + + # Non-block keys (embedders, modulations, output layers) + if bfl_key in _NON_BLOCK_RENAMES: + return [(_NON_BLOCK_RENAMES[bfl_key], layer)] + + # Fallback: pass through unchanged + return [(bfl_key, layer)] + + +def _split_qkv_lora_layer( + layer: LoRALayer, + q_key: str, + k_key: str, + v_key: str, +) -> list[tuple[str, LoRALayer]]: + """Split a fused QKV LoRALayer into separate Q, K, V LoRALayers. + + The up weight [3*hidden, rank] is split into 3 parts. + The down weight [rank, hidden] is cloned for each. + """ + up_q, up_k, up_v = layer.up.chunk(3, dim=0) + + result = [] + for key, up_part in [(q_key, up_q), (k_key, up_k), (v_key, up_v)]: + split_layer = LoRALayer( + up=up_part, + mid=None, + down=layer.down.clone(), + alpha=layer._alpha, + bias=None, + ) + result.append((key, split_layer)) + + return result diff --git a/invokeai/backend/patches/lora_conversions/flux_control_lora_utils.py b/invokeai/backend/patches/lora_conversions/flux_control_lora_utils.py new file mode 100644 index 00000000000..1762a4d5f4c --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/flux_control_lora_utils.py @@ -0,0 +1,86 @@ +import re +from typing import Any, Dict + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.flux_control_lora_layer import FluxControlLoRALayer +from invokeai.backend.patches.layers.lora_layer import LoRALayer +from invokeai.backend.patches.layers.set_parameter_layer import SetParameterLayer +from invokeai.backend.patches.lora_conversions.flux_lora_constants import FLUX_LORA_TRANSFORMER_PREFIX +from invokeai.backend.patches.model_patch_raw import ModelPatchRaw + +# A regex pattern that matches all of the keys in the Flux Dev/Canny LoRA format. +# Example keys: +# guidance_in.in_layer.lora_B.bias +# single_blocks.0.linear1.lora_A.weight +# double_blocks.0.img_attn.norm.key_norm.scale +FLUX_CONTROL_TRANSFORMER_KEY_REGEX = r"(\w+\.)+(lora_A\.weight|lora_B\.weight|lora_B\.bias|scale)" + + +def is_state_dict_likely_flux_control(state_dict: dict[str | int, Any]) -> bool: + """Checks if the provided state dict is likely in the FLUX Control LoRA format. + + This is intended to be a high-precision detector, but it is not guaranteed to have perfect precision. (A + perfect-precision detector would require checking all keys against a whitelist and verifying tensor shapes.) + """ + + all_keys_match = all( + re.match(FLUX_CONTROL_TRANSFORMER_KEY_REGEX, k) for k in state_dict.keys() if isinstance(k, str) + ) + + # Check the shape of the img_in weight, because this layer shape is modified by FLUX control LoRAs. + lora_a_weight = state_dict.get("img_in.lora_A.weight", None) + lora_b_bias = state_dict.get("img_in.lora_B.bias", None) + lora_b_weight = state_dict.get("img_in.lora_B.weight", None) + + return ( + all_keys_match + and lora_a_weight is not None + and lora_b_bias is not None + and lora_b_weight is not None + and lora_a_weight.shape[1] == 128 + and lora_b_weight.shape[0] == 3072 + and lora_b_bias.shape[0] == 3072 + ) + + +def lora_model_from_flux_control_state_dict(state_dict: Dict[str, torch.Tensor]) -> ModelPatchRaw: + # Group keys by layer. + grouped_state_dict: dict[str, dict[str, torch.Tensor]] = {} + for key, value in state_dict.items(): + key_props = key.split(".") + layer_prop_size = -2 if any(prop in key for prop in ["lora_B", "lora_A"]) else -1 + layer_name = ".".join(key_props[:layer_prop_size]) + param_name = ".".join(key_props[layer_prop_size:]) + if layer_name not in grouped_state_dict: + grouped_state_dict[layer_name] = {} + grouped_state_dict[layer_name][param_name] = value + + # Create LoRA layers. + layers: dict[str, BaseLayerPatch] = {} + for layer_key, layer_state_dict in grouped_state_dict.items(): + prefixed_key = f"{FLUX_LORA_TRANSFORMER_PREFIX}{layer_key}" + if layer_key == "img_in": + # img_in is a special case because it changes the shape of the original weight. + layers[prefixed_key] = FluxControlLoRALayer( + layer_state_dict["lora_B.weight"], + None, + layer_state_dict["lora_A.weight"], + None, + layer_state_dict["lora_B.bias"], + ) + elif all(k in layer_state_dict for k in ["lora_A.weight", "lora_B.bias", "lora_B.weight"]): + layers[prefixed_key] = LoRALayer( + layer_state_dict["lora_B.weight"], + None, + layer_state_dict["lora_A.weight"], + None, + layer_state_dict["lora_B.bias"], + ) + elif "scale" in layer_state_dict: + layers[prefixed_key] = SetParameterLayer("scale", layer_state_dict["scale"]) + else: + raise ValueError(f"{layer_key} not expected") + + return ModelPatchRaw(layers=layers) diff --git a/invokeai/backend/patches/lora_conversions/flux_diffusers_lora_conversion_utils.py b/invokeai/backend/patches/lora_conversions/flux_diffusers_lora_conversion_utils.py new file mode 100644 index 00000000000..05fe4cab297 --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/flux_diffusers_lora_conversion_utils.py @@ -0,0 +1,384 @@ +from typing import Dict + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.merged_layer_patch import MergedLayerPatch, Range +from invokeai.backend.patches.layers.utils import any_lora_layer_from_state_dict +from invokeai.backend.patches.lora_conversions.flux_lora_constants import FLUX_LORA_TRANSFORMER_PREFIX +from invokeai.backend.patches.model_patch_raw import ModelPatchRaw + + +def is_state_dict_likely_in_flux_diffusers_format(state_dict: dict[str | int, torch.Tensor]) -> bool: + """Checks if the provided state dict is likely in the Diffusers FLUX LoRA format. + + This detects both Flux.1 diffusers format (separate to_q/to_k/to_v, ff.net.0.proj) and + Flux2 Klein diffusers format (fused to_qkv_mlp_proj, ff.linear_in). + + This is intended to be a reasonably high-precision detector, but it is not guaranteed to have perfect precision. (A + perfect-precision detector would require checking all keys against a whitelist and verifying tensor shapes.) + """ + # Check that all keys are LoRA weight keys (either PEFT or standard format). + # Some LoRAs use a mix of formats (PEFT for some layers, standard for others). + _LORA_SUFFIXES = ("lora_A.weight", "lora_B.weight", "lora.down.weight", "lora.up.weight") + all_keys_are_lora = all(k.endswith(_LORA_SUFFIXES) for k in state_dict.keys() if isinstance(k, str)) + if not all_keys_are_lora: + return False + + # --- Flux.1 diffusers key patterns (separate Q/K/V, ff.net.0.proj) --- + # Check if keys use transformer prefix + flux1_transformer_keys = [ + "transformer.single_transformer_blocks.0.attn.to_q.lora_A.weight", + "transformer.single_transformer_blocks.0.attn.to_q.lora_B.weight", + "transformer.transformer_blocks.0.attn.add_q_proj.lora_A.weight", + "transformer.transformer_blocks.0.attn.add_q_proj.lora_B.weight", + ] + flux1_transformer_present = all(k in state_dict for k in flux1_transformer_keys) + + # Check if keys use base_model.model prefix + flux1_base_model_keys = [ + "base_model.model.single_transformer_blocks.0.attn.to_q.lora_A.weight", + "base_model.model.single_transformer_blocks.0.attn.to_q.lora_B.weight", + "base_model.model.transformer_blocks.0.attn.add_q_proj.lora_A.weight", + "base_model.model.transformer_blocks.0.attn.add_q_proj.lora_B.weight", + ] + flux1_base_model_present = all(k in state_dict for k in flux1_base_model_keys) + + if flux1_transformer_present or flux1_base_model_present: + return True + + # --- Flux2 Klein diffusers key patterns (fused QKV+MLP, ff.linear_in) --- + # These use Flux2Transformer2DModel naming which differs from Flux.1. + # An empty prefix is supported because some trainers (e.g. PEFT-style LoRAs from + # Modelscope/MuseAI Klein 9B finetunes) save keys at the top level without any + # `transformer.` or `base_model.model.` wrapper. + for prefix in ["transformer.", "base_model.model.", ""]: + has_single = any( + k.startswith(f"{prefix}single_transformer_blocks.") and "to_qkv_mlp_proj" in k for k in state_dict + ) + has_double = any(k.startswith(f"{prefix}transformer_blocks.") for k in state_dict if isinstance(k, str)) + if has_single or has_double: + # Verify it's actually Flux2 naming by checking for a Flux2-specific key pattern. + # Flux2 uses ff.linear_in (not ff.net.0.proj) and attn.to_add_out (not attn.to_add_out in Flux.1 too, + # but fused to_qkv_mlp_proj is unique to Flux2). + has_flux2_keys = any( + ("to_qkv_mlp_proj" in k or "ff.linear_in" in k or "ff_context.linear_in" in k) + for k in state_dict + if isinstance(k, str) + ) + if has_flux2_keys: + return True + + return False + + +def is_state_dict_flux2_diffusers_format(state_dict: dict[str | int, torch.Tensor]) -> bool: + """Checks if the state dict uses Flux2 Klein native diffusers naming (not Flux.1 diffusers naming). + + Returns True only for Flux2 Klein diffusers format (to_qkv_mlp_proj, ff.linear_in, etc.), + NOT for Flux.1 diffusers format (to_q/to_k/to_v, ff.net.0.proj). + """ + str_keys = [k for k in state_dict.keys() if isinstance(k, str)] + return any("to_qkv_mlp_proj" in k or "ff.linear_in" in k or "ff_context.linear_in" in k for k in str_keys) + + +def lora_model_from_flux_diffusers_state_dict( + state_dict: Dict[str, torch.Tensor], alpha: float | None +) -> ModelPatchRaw: + # Group keys by layer. + grouped_state_dict: dict[str, dict[str, torch.Tensor]] = _group_by_layer(state_dict) + layers = lora_layers_from_flux_diffusers_grouped_state_dict(grouped_state_dict, alpha) + return ModelPatchRaw(layers=layers) + + +def lora_layers_from_flux_diffusers_grouped_state_dict( + grouped_state_dict: Dict[str, Dict[str, torch.Tensor]], alpha: float | None +) -> dict[str, BaseLayerPatch]: + """Converts a grouped state dict with Diffusers FLUX LoRA keys to LoRA layers with BFL keys (i.e. the module key + format used by Invoke). + + This function is based on: + https://github.com/huggingface/diffusers/blob/55ac421f7bb12fd00ccbef727be4dc2f3f920abb/scripts/convert_flux_to_diffusers.py + """ + + # Determine which prefix is used and remove it from all keys. + # Check if any key starts with "base_model.model." prefix + has_base_model_prefix = any(k.startswith("base_model.model.") for k in grouped_state_dict.keys()) + + if has_base_model_prefix: + # Remove the "base_model.model." prefix from all keys. + grouped_state_dict = {k.replace("base_model.model.", ""): v for k, v in grouped_state_dict.items()} + else: + # Remove the "transformer." prefix from all keys. + grouped_state_dict = {k.replace("transformer.", ""): v for k, v in grouped_state_dict.items()} + + # Constants for FLUX.1 + num_double_layers = 19 + num_single_layers = 38 + hidden_size = 3072 + mlp_ratio = 4.0 + mlp_hidden_dim = int(hidden_size * mlp_ratio) + + layers: dict[str, BaseLayerPatch] = {} + + def get_lora_layer_values(src_layer_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: + if "lora_A.weight" in src_layer_dict: + # The LoRA keys are in PEFT format. + values = { + "lora_down.weight": src_layer_dict.pop("lora_A.weight"), + "lora_up.weight": src_layer_dict.pop("lora_B.weight"), + } + if alpha is not None: + values["alpha"] = torch.tensor(alpha) + assert len(src_layer_dict) == 0 + return values + else: + # Assume that the LoRA keys are in Kohya format. + return src_layer_dict + + def add_lora_layer_if_present(src_key: str, dst_key: str) -> None: + if src_key in grouped_state_dict: + src_layer_dict = grouped_state_dict.pop(src_key) + values = get_lora_layer_values(src_layer_dict) + layers[dst_key] = any_lora_layer_from_state_dict(values) + + def add_qkv_lora_layer_if_present( + src_keys: list[str], + src_weight_shapes: list[tuple[int, int]], + dst_qkv_key: str, + allow_missing_keys: bool = False, + ) -> None: + """Handle the Q, K, V matrices for a transformer block. We need special handling because the diffusers format + stores them in separate matrices, whereas the BFL format used internally by InvokeAI concatenates them. + """ + # If none of the keys are present, return early. + keys_present = [key in grouped_state_dict for key in src_keys] + if not any(keys_present): + return + + dim_0_offset = 0 + sub_layers: list[BaseLayerPatch] = [] + sub_layer_ranges: list[Range] = [] + for src_key, src_weight_shape in zip(src_keys, src_weight_shapes, strict=True): + src_layer_dict = grouped_state_dict.pop(src_key, None) + if src_layer_dict is not None: + values = get_lora_layer_values(src_layer_dict) + # assert values["lora_down.weight"].shape[1] == src_weight_shape[1] + # assert values["lora_up.weight"].shape[0] == src_weight_shape[0] + sub_layers.append(any_lora_layer_from_state_dict(values)) + sub_layer_ranges.append(Range(dim_0_offset, dim_0_offset + src_weight_shape[0])) + else: + if not allow_missing_keys: + raise ValueError(f"Missing LoRA layer: '{src_key}'.") + + dim_0_offset += src_weight_shape[0] + + layers[dst_qkv_key] = MergedLayerPatch(sub_layers, sub_layer_ranges) + + # time_text_embed.timestep_embedder -> time_in. + add_lora_layer_if_present("time_text_embed.timestep_embedder.linear_1", "time_in.in_layer") + add_lora_layer_if_present("time_text_embed.timestep_embedder.linear_2", "time_in.out_layer") + + # time_text_embed.text_embedder -> vector_in. + add_lora_layer_if_present("time_text_embed.text_embedder.linear_1", "vector_in.in_layer") + add_lora_layer_if_present("time_text_embed.text_embedder.linear_2", "vector_in.out_layer") + + # time_text_embed.guidance_embedder -> guidance_in. + add_lora_layer_if_present("time_text_embed.guidance_embedder.linear_1", "guidance_in") + add_lora_layer_if_present("time_text_embed.guidance_embedder.linear_2", "guidance_in") + + # context_embedder -> txt_in. + add_lora_layer_if_present("context_embedder", "txt_in") + + # x_embedder -> img_in. + add_lora_layer_if_present("x_embedder", "img_in") + + # Double transformer blocks. + for i in range(num_double_layers): + # norms. + add_lora_layer_if_present(f"transformer_blocks.{i}.norm1.linear", f"double_blocks.{i}.img_mod.lin") + add_lora_layer_if_present(f"transformer_blocks.{i}.norm1_context.linear", f"double_blocks.{i}.txt_mod.lin") + + # Q, K, V + add_qkv_lora_layer_if_present( + [ + f"transformer_blocks.{i}.attn.to_q", + f"transformer_blocks.{i}.attn.to_k", + f"transformer_blocks.{i}.attn.to_v", + ], + [(hidden_size, hidden_size), (hidden_size, hidden_size), (hidden_size, hidden_size)], + f"double_blocks.{i}.img_attn.qkv", + ) + add_qkv_lora_layer_if_present( + [ + f"transformer_blocks.{i}.attn.add_q_proj", + f"transformer_blocks.{i}.attn.add_k_proj", + f"transformer_blocks.{i}.attn.add_v_proj", + ], + [(hidden_size, hidden_size), (hidden_size, hidden_size), (hidden_size, hidden_size)], + f"double_blocks.{i}.txt_attn.qkv", + ) + + # ff img_mlp + add_lora_layer_if_present( + f"transformer_blocks.{i}.ff.net.0.proj", + f"double_blocks.{i}.img_mlp.0", + ) + add_lora_layer_if_present( + f"transformer_blocks.{i}.ff.net.2", + f"double_blocks.{i}.img_mlp.2", + ) + + # ff txt_mlp + add_lora_layer_if_present( + f"transformer_blocks.{i}.ff_context.net.0.proj", + f"double_blocks.{i}.txt_mlp.0", + ) + add_lora_layer_if_present( + f"transformer_blocks.{i}.ff_context.net.2", + f"double_blocks.{i}.txt_mlp.2", + ) + + # output projections. + add_lora_layer_if_present( + f"transformer_blocks.{i}.attn.to_out.0", + f"double_blocks.{i}.img_attn.proj", + ) + add_lora_layer_if_present( + f"transformer_blocks.{i}.attn.to_add_out", + f"double_blocks.{i}.txt_attn.proj", + ) + + # Single transformer blocks. + for i in range(num_single_layers): + # norms + add_lora_layer_if_present( + f"single_transformer_blocks.{i}.norm.linear", + f"single_blocks.{i}.modulation.lin", + ) + + # Q, K, V, mlp + add_qkv_lora_layer_if_present( + [ + f"single_transformer_blocks.{i}.attn.to_q", + f"single_transformer_blocks.{i}.attn.to_k", + f"single_transformer_blocks.{i}.attn.to_v", + f"single_transformer_blocks.{i}.proj_mlp", + ], + [ + (hidden_size, hidden_size), + (hidden_size, hidden_size), + (hidden_size, hidden_size), + (mlp_hidden_dim, hidden_size), + ], + f"single_blocks.{i}.linear1", + allow_missing_keys=True, + ) + + # Output projections. + add_lora_layer_if_present( + f"single_transformer_blocks.{i}.proj_out", + f"single_blocks.{i}.linear2", + ) + + # Final layer. + add_lora_layer_if_present("proj_out", "final_layer.linear") + + # Assert that all keys were processed. + assert len(grouped_state_dict) == 0 + + layers_with_prefix = {f"{FLUX_LORA_TRANSFORMER_PREFIX}{k}": v for k, v in layers.items()} + + return layers_with_prefix + + +def lora_model_from_flux2_diffusers_state_dict( + state_dict: Dict[str, torch.Tensor], alpha: float | None +) -> ModelPatchRaw: + """Convert a Flux2 Klein native diffusers format LoRA state dict to a ModelPatchRaw. + + Flux2 Klein diffusers LoRAs use key names that match Flux2Transformer2DModel directly + (e.g. transformer_blocks.0.attn.to_add_out, single_transformer_blocks.0.attn.to_qkv_mlp_proj). + The conversion strips the model prefix (transformer. or base_model.model.) and adds + the InvokeAI prefix. + + Some LoRAs use a mix of PEFT format (lora_A.weight/lora_B.weight) and standard format + (lora.down.weight/lora.up.weight) for different layers. Both are handled here. + """ + grouped_state_dict = _group_by_layer_mixed_format(state_dict) + + # Determine and strip prefix + has_base_model_prefix = any(k.startswith("base_model.model.") for k in grouped_state_dict.keys()) + if has_base_model_prefix: + grouped_state_dict = {k.replace("base_model.model.", "", 1): v for k, v in grouped_state_dict.items()} + else: + grouped_state_dict = {k.replace("transformer.", "", 1): v for k, v in grouped_state_dict.items()} + + layers: dict[str, BaseLayerPatch] = {} + for layer_key, src_layer_dict in grouped_state_dict.items(): + # Normalize to InvokeAI naming (lora_down.weight / lora_up.weight) + values: dict[str, torch.Tensor] = {} + if "lora_A.weight" in src_layer_dict: + values["lora_down.weight"] = src_layer_dict["lora_A.weight"] + values["lora_up.weight"] = src_layer_dict["lora_B.weight"] + elif "lora.down.weight" in src_layer_dict: + values["lora_down.weight"] = src_layer_dict["lora.down.weight"] + values["lora_up.weight"] = src_layer_dict["lora.up.weight"] + else: + values = src_layer_dict + + if alpha is not None and "alpha" not in values: + values["alpha"] = torch.tensor(alpha) + + layers[f"{FLUX_LORA_TRANSFORMER_PREFIX}{layer_key}"] = any_lora_layer_from_state_dict(values) + + return ModelPatchRaw(layers=layers) + + +def _group_by_layer_mixed_format(state_dict: Dict[str, torch.Tensor]) -> dict[str, dict[str, torch.Tensor]]: + """Groups keys by layer, handling both PEFT and standard LoRA suffixes. + + PEFT format: layer_name.lora_A.weight → layer=layer_name, suffix=lora_A.weight + Standard format: layer_name.lora.down.weight → layer=layer_name, suffix=lora.down.weight + """ + layer_dict: dict[str, dict[str, torch.Tensor]] = {} + for key in state_dict: + if not isinstance(key, str): + continue + + # Determine suffix length based on the key ending + if key.endswith((".lora_A.weight", ".lora_B.weight")): + # PEFT format: split off 2 parts (lora_A + weight) + parts = key.rsplit(".", maxsplit=2) + layer_name = parts[0] + suffix = ".".join(parts[1:]) + elif key.endswith((".lora.down.weight", ".lora.up.weight")): + # Standard format: split off 3 parts (lora + down/up + weight) + parts = key.rsplit(".", maxsplit=3) + layer_name = parts[0] + suffix = ".".join(parts[1:]) + else: + # Unknown format, use 2-part split as fallback + parts = key.rsplit(".", maxsplit=2) + layer_name = parts[0] + suffix = ".".join(parts[1:]) + + if layer_name not in layer_dict: + layer_dict[layer_name] = {} + layer_dict[layer_name][suffix] = state_dict[key] + + return layer_dict + + +def _group_by_layer(state_dict: Dict[str, torch.Tensor]) -> dict[str, dict[str, torch.Tensor]]: + """Groups the keys in the state dict by layer.""" + layer_dict: dict[str, dict[str, torch.Tensor]] = {} + for key in state_dict: + # Split the 'lora_A.weight' or 'lora_B.weight' suffix from the layer name. + parts = key.rsplit(".", maxsplit=2) + layer_name = parts[0] + key_name = ".".join(parts[1:]) + if layer_name not in layer_dict: + layer_dict[layer_name] = {} + layer_dict[layer_name][key_name] = state_dict[key] + return layer_dict diff --git a/invokeai/backend/patches/lora_conversions/flux_kohya_lora_conversion_utils.py b/invokeai/backend/patches/lora_conversions/flux_kohya_lora_conversion_utils.py new file mode 100644 index 00000000000..f5a6830c4f1 --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/flux_kohya_lora_conversion_utils.py @@ -0,0 +1,184 @@ +import re +from typing import Any, Dict, TypeVar + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.utils import any_lora_layer_from_state_dict +from invokeai.backend.patches.lora_conversions.flux_lora_constants import ( + FLUX_LORA_CLIP_PREFIX, + FLUX_LORA_T5_PREFIX, + FLUX_LORA_TRANSFORMER_PREFIX, +) +from invokeai.backend.patches.model_patch_raw import ModelPatchRaw + +# A regex pattern that matches all of the transformer keys in the Kohya FLUX LoRA format. +# Example keys: +# lora_unet_double_blocks_0_img_attn_proj.alpha +# lora_unet_double_blocks_0_img_attn_proj.lora_down.weight +# lora_unet_double_blocks_0_img_attn_proj.lora_up.weight +FLUX_KOHYA_TRANSFORMER_KEY_REGEX = ( + r"lora_unet_(\w+_blocks)_(\d+)_(img_attn|img_mlp|img_mod|txt_attn|txt_mlp|txt_mod|linear1|linear2|modulation)_?(.*)" +) + +# A regex pattern that matches all of the last layer keys in the Kohya FLUX LoRA format. +# Example keys: +# lora_unet_final_layer_linear.alpha +# lora_unet_final_layer_linear.lora_down.weight +# lora_unet_final_layer_linear.lora_up.weight +FLUX_KOHYA_LAST_LAYER_KEY_REGEX = r"lora_unet_final_layer_(linear|linear1|linear2)_?(.*)" + +# A regex pattern that matches all of the CLIP keys in the Kohya FLUX LoRA format. +# Example keys: +# lora_te1_text_model_encoder_layers_0_mlp_fc1.alpha +# lora_te1_text_model_encoder_layers_0_mlp_fc1.lora_down.weight +# lora_te1_text_model_encoder_layers_0_mlp_fc1.lora_up.weight +FLUX_KOHYA_CLIP_KEY_REGEX = r"lora_te1_text_model_encoder_layers_(\d+)_(mlp|self_attn)_(\w+)\.?.*" + +# A regex pattern that matches all of the T5 keys in the Kohya FLUX LoRA format. +# Example keys: +# lora_te2_encoder_block_0_layer_0_SelfAttention_k.alpha +# lora_te2_encoder_block_0_layer_0_SelfAttention_k.dora_scale +# lora_te2_encoder_block_0_layer_0_SelfAttention_k.lora_down.weight +# lora_te2_encoder_block_0_layer_0_SelfAttention_k.lora_up.weight +FLUX_KOHYA_T5_KEY_REGEX = r"lora_te2_encoder_block_(\d+)_layer_(\d+)_(DenseReluDense|SelfAttention)_(\w+)_?(\w+)?\.?.*" + + +def is_state_dict_likely_in_flux_kohya_format(state_dict: dict[str | int, Any]) -> bool: + """Checks if the provided state dict is likely in the Kohya FLUX LoRA format. + + This is intended to be a high-precision detector, but it is not guaranteed to have perfect precision. (A + perfect-precision detector would require checking all keys against a whitelist and verifying tensor shapes.) + """ + return all( + re.match(FLUX_KOHYA_TRANSFORMER_KEY_REGEX, k) + or re.match(FLUX_KOHYA_LAST_LAYER_KEY_REGEX, k) + or re.match(FLUX_KOHYA_CLIP_KEY_REGEX, k) + or re.match(FLUX_KOHYA_T5_KEY_REGEX, k) + for k in state_dict.keys() + if isinstance(k, str) + ) + + +def lora_model_from_flux_kohya_state_dict(state_dict: Dict[str, torch.Tensor]) -> ModelPatchRaw: + # Group keys by layer. + grouped_state_dict: dict[str, dict[str, torch.Tensor]] = {} + for key, value in state_dict.items(): + layer_name, param_name = key.split(".", 1) + if layer_name not in grouped_state_dict: + grouped_state_dict[layer_name] = {} + grouped_state_dict[layer_name][param_name] = value + + # Split the grouped state dict into transformer, CLIP, and T5 state dicts. + transformer_grouped_sd: dict[str, dict[str, torch.Tensor]] = {} + clip_grouped_sd: dict[str, dict[str, torch.Tensor]] = {} + t5_grouped_sd: dict[str, dict[str, torch.Tensor]] = {} + for layer_name, layer_state_dict in grouped_state_dict.items(): + if layer_name.startswith("lora_unet"): + # Skip the final layer. This is incompatible with current model definition. + if layer_name.startswith("lora_unet_final_layer"): + continue + transformer_grouped_sd[layer_name] = layer_state_dict + elif layer_name.startswith("lora_te1"): + clip_grouped_sd[layer_name] = layer_state_dict + elif layer_name.startswith("lora_te2"): + t5_grouped_sd[layer_name] = layer_state_dict + else: + raise ValueError(f"Layer '{layer_name}' does not match the expected pattern for FLUX LoRA weights.") + + # Convert the state dicts to the InvokeAI format. + transformer_grouped_sd = _convert_flux_transformer_kohya_state_dict_to_invoke_format(transformer_grouped_sd) + clip_grouped_sd = _convert_flux_clip_kohya_state_dict_to_invoke_format(clip_grouped_sd) + t5_grouped_sd = _convert_flux_t5_kohya_state_dict_to_invoke_format(t5_grouped_sd) + + # Create LoRA layers. + layers: dict[str, BaseLayerPatch] = {} + for model_prefix, grouped_sd in [ + (FLUX_LORA_TRANSFORMER_PREFIX, transformer_grouped_sd), + (FLUX_LORA_CLIP_PREFIX, clip_grouped_sd), + (FLUX_LORA_T5_PREFIX, t5_grouped_sd), + ]: + for layer_key, layer_state_dict in grouped_sd.items(): + layers[model_prefix + layer_key] = any_lora_layer_from_state_dict(layer_state_dict) + + # Create and return the LoRAModelRaw. + return ModelPatchRaw(layers=layers) + + +T = TypeVar("T") + + +def _convert_flux_clip_kohya_state_dict_to_invoke_format(state_dict: Dict[str, T]) -> Dict[str, T]: + """Converts a CLIP LoRA state dict from the Kohya FLUX LoRA format to LoRA weight format used internally by + InvokeAI. + + Example key conversions: + + "lora_te1_text_model_encoder_layers_0_mlp_fc1" -> "text_model.encoder.layers.0.mlp.fc1", + "lora_te1_text_model_encoder_layers_0_self_attn_k_proj" -> "text_model.encoder.layers.0.self_attn.k_proj" + """ + converted_sd: dict[str, T] = {} + for k, v in state_dict.items(): + match = re.match(FLUX_KOHYA_CLIP_KEY_REGEX, k) + if match: + new_key = f"text_model.encoder.layers.{match.group(1)}.{match.group(2)}.{match.group(3)}" + converted_sd[new_key] = v + else: + raise ValueError(f"Key '{k}' does not match the expected pattern for FLUX LoRA weights.") + + return converted_sd + + +def _convert_flux_transformer_kohya_state_dict_to_invoke_format(state_dict: Dict[str, T]) -> Dict[str, T]: + """Converts a FLUX tranformer LoRA state dict from the Kohya FLUX LoRA format to LoRA weight format used internally + by InvokeAI. + + Example key conversions: + "lora_unet_double_blocks_0_img_attn_proj" -> "double_blocks.0.img_attn.proj" + "lora_unet_double_blocks_0_img_attn_qkv" -> "double_blocks.0.img_attn.qkv" + """ + + def replace_func(match: re.Match[str]) -> str: + s = f"{match.group(1)}.{match.group(2)}.{match.group(3)}" + if match.group(4): + s += f".{match.group(4)}" + return s + + converted_dict: dict[str, T] = {} + for k, v in state_dict.items(): + match = re.match(FLUX_KOHYA_TRANSFORMER_KEY_REGEX, k) + if match: + new_key = re.sub(FLUX_KOHYA_TRANSFORMER_KEY_REGEX, replace_func, k) + converted_dict[new_key] = v + else: + raise ValueError(f"Key '{k}' does not match the expected pattern for FLUX LoRA weights.") + + return converted_dict + + +def _convert_flux_t5_kohya_state_dict_to_invoke_format(state_dict: Dict[str, T]) -> Dict[str, T]: + """Converts a T5 LoRA state dict from the Kohya FLUX LoRA format to LoRA weight format used internally by + InvokeAI. + + Example key conversions: + + "lora_te2_encoder_block_0_layer_0_SelfAttention_k" -> "encoder.block.0.layer.0.SelfAttention.k" + "lora_te2_encoder_block_0_layer_1_DenseReluDense_wi_0" -> "encoder.block.0.layer.1.DenseReluDense.wi.0" + """ + + def replace_func(match: re.Match[str]) -> str: + s = f"encoder.block.{match.group(1)}.layer.{match.group(2)}.{match.group(3)}.{match.group(4)}" + if match.group(5): + s += f".{match.group(5)}" + return s + + converted_dict: dict[str, T] = {} + for k, v in state_dict.items(): + match = re.match(FLUX_KOHYA_T5_KEY_REGEX, k) + if match: + new_key = re.sub(FLUX_KOHYA_T5_KEY_REGEX, replace_func, k) + converted_dict[new_key] = v + else: + raise ValueError(f"Key '{k}' does not match the expected pattern for FLUX LoRA weights.") + + return converted_dict diff --git a/invokeai/backend/patches/lora_conversions/flux_lora_constants.py b/invokeai/backend/patches/lora_conversions/flux_lora_constants.py new file mode 100644 index 00000000000..28575144627 --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/flux_lora_constants.py @@ -0,0 +1,4 @@ +# Prefixes used to distinguish between transformer and CLIP text encoder keys in the FLUX InvokeAI LoRA format. +FLUX_LORA_TRANSFORMER_PREFIX = "lora_transformer-" +FLUX_LORA_CLIP_PREFIX = "lora_clip-" +FLUX_LORA_T5_PREFIX = "lora_t5-" diff --git a/invokeai/backend/patches/lora_conversions/flux_onetrainer_bfl_lora_conversion_utils.py b/invokeai/backend/patches/lora_conversions/flux_onetrainer_bfl_lora_conversion_utils.py new file mode 100644 index 00000000000..b2109222a31 --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/flux_onetrainer_bfl_lora_conversion_utils.py @@ -0,0 +1,168 @@ +"""Utilities for detecting and converting FLUX LoRAs in OneTrainer BFL format. + +This format is produced by newer versions of OneTrainer and uses BFL internal key names +(double_blocks, single_blocks, img_attn, etc.) with a 'transformer.' prefix and +InvokeAI-native LoRA suffixes (lora_down.weight, lora_up.weight, alpha). + +Unlike the standard BFL PEFT format (which uses 'diffusion_model.' prefix and lora_A/lora_B), +this format also has split QKV projections: + - double_blocks.{i}.img_attn.qkv.{0,1,2} (Q, K, V separate) + - double_blocks.{i}.txt_attn.qkv.{0,1,2} (Q, K, V separate) + - single_blocks.{i}.linear1.{0,1,2,3} (Q, K, V, MLP separate) + +Example keys: + transformer.double_blocks.0.img_attn.qkv.0.lora_down.weight + transformer.double_blocks.0.img_attn.qkv.0.lora_up.weight + transformer.double_blocks.0.img_attn.qkv.0.alpha + transformer.single_blocks.0.linear1.3.lora_down.weight + transformer.double_blocks.0.img_mlp.0.lora_down.weight +""" + +import re +from typing import Any, Dict + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.merged_layer_patch import MergedLayerPatch, Range +from invokeai.backend.patches.layers.utils import any_lora_layer_from_state_dict +from invokeai.backend.patches.lora_conversions.flux_lora_constants import FLUX_LORA_TRANSFORMER_PREFIX +from invokeai.backend.patches.model_patch_raw import ModelPatchRaw + +_TRANSFORMER_PREFIX = "transformer." + +# Valid LoRA weight suffixes in this format. +_LORA_SUFFIXES = ("lora_down.weight", "lora_up.weight", "alpha") + +# Regex to detect split QKV keys in double blocks: e.g. "double_blocks.0.img_attn.qkv.1" +_SPLIT_QKV_RE = re.compile(r"^(double_blocks\.\d+\.(img_attn|txt_attn)\.qkv)\.\d+$") + +# Regex to detect split linear1 keys in single blocks: e.g. "single_blocks.0.linear1.2" +_SPLIT_LINEAR1_RE = re.compile(r"^(single_blocks\.\d+\.linear1)\.\d+$") + + +def is_state_dict_likely_in_flux_onetrainer_bfl_format( + state_dict: dict[str | int, Any], + metadata: dict[str, Any] | None = None, +) -> bool: + """Checks if the provided state dict is likely in the OneTrainer BFL FLUX LoRA format. + + This format uses BFL internal key names with 'transformer.' prefix and split QKV projections. + """ + str_keys = [k for k in state_dict.keys() if isinstance(k, str)] + if not str_keys: + return False + + # All keys must start with 'transformer.' + if not all(k.startswith(_TRANSFORMER_PREFIX) for k in str_keys): + return False + + # All keys must end with recognized LoRA suffixes. + if not all(k.endswith(_LORA_SUFFIXES) for k in str_keys): + return False + + # Must have BFL block structure (double_blocks or single_blocks) under transformer prefix. + has_bfl_blocks = any( + k.startswith("transformer.double_blocks.") or k.startswith("transformer.single_blocks.") for k in str_keys + ) + if not has_bfl_blocks: + return False + + # Must have split QKV pattern (qkv.0, qkv.1, qkv.2) to distinguish from other formats + # that might use transformer. prefix in the future. + has_split_qkv = any(".qkv.0." in k or ".qkv.1." in k or ".qkv.2." in k or ".linear1.0." in k for k in str_keys) + if not has_split_qkv: + return False + + return True + + +def _split_key(key: str) -> tuple[str, str]: + """Split a key into (layer_name, weight_suffix). + + Handles: + - 2-component suffixes ending with '.weight': e.g., 'lora_down.weight' → split at 2nd-to-last dot + - 1-component suffixes: e.g., 'alpha' → split at last dot + """ + if key.endswith(".weight"): + parts = key.rsplit(".", maxsplit=2) + return parts[0], f"{parts[1]}.{parts[2]}" + else: + parts = key.rsplit(".", maxsplit=1) + return parts[0], parts[1] + + +def lora_model_from_flux_onetrainer_bfl_state_dict(state_dict: Dict[str, torch.Tensor]) -> ModelPatchRaw: + """Convert a OneTrainer BFL format FLUX LoRA state dict to a ModelPatchRaw. + + Strips the 'transformer.' prefix, groups by layer, and merges split QKV/linear1 + layers into MergedLayerPatch instances. + """ + # Step 1: Strip prefix and group by layer name. + grouped_state_dict: dict[str, dict[str, torch.Tensor]] = {} + for key, value in state_dict.items(): + if not isinstance(key, str): + continue + + # Strip 'transformer.' prefix. + key = key[len(_TRANSFORMER_PREFIX) :] + + layer_name, suffix = _split_key(key) + + if layer_name not in grouped_state_dict: + grouped_state_dict[layer_name] = {} + grouped_state_dict[layer_name][suffix] = value + + # Step 2: Build LoRA layers, merging split QKV and linear1. + layers: dict[str, BaseLayerPatch] = {} + + # Identify which layers need merging. + merge_groups: dict[str, list[str]] = {} + standalone_keys: list[str] = [] + + for layer_key in grouped_state_dict: + qkv_match = _SPLIT_QKV_RE.match(layer_key) + linear1_match = _SPLIT_LINEAR1_RE.match(layer_key) + + if qkv_match: + parent = qkv_match.group(1) + if parent not in merge_groups: + merge_groups[parent] = [] + merge_groups[parent].append(layer_key) + elif linear1_match: + parent = linear1_match.group(1) + if parent not in merge_groups: + merge_groups[parent] = [] + merge_groups[parent].append(layer_key) + else: + standalone_keys.append(layer_key) + + # Process standalone layers. + for layer_key in standalone_keys: + layer_sd = grouped_state_dict[layer_key] + layers[f"{FLUX_LORA_TRANSFORMER_PREFIX}{layer_key}"] = any_lora_layer_from_state_dict(layer_sd) + + # Process merged layers. + for parent_key, sub_keys in merge_groups.items(): + # Sort by the numeric index at the end (e.g., qkv.0, qkv.1, qkv.2). + sub_keys.sort(key=lambda k: int(k.rsplit(".", maxsplit=1)[1])) + + sub_layers: list[BaseLayerPatch] = [] + sub_ranges: list[Range] = [] + dim_0_offset = 0 + + for sub_key in sub_keys: + layer_sd = grouped_state_dict[sub_key] + sub_layer = any_lora_layer_from_state_dict(layer_sd) + + # Determine the output dimension from the up weight shape. + up_weight = layer_sd["lora_up.weight"] + out_dim = up_weight.shape[0] + + sub_layers.append(sub_layer) + sub_ranges.append(Range(dim_0_offset, dim_0_offset + out_dim)) + dim_0_offset += out_dim + + layers[f"{FLUX_LORA_TRANSFORMER_PREFIX}{parent_key}"] = MergedLayerPatch(sub_layers, sub_ranges) + + return ModelPatchRaw(layers=layers) diff --git a/invokeai/backend/patches/lora_conversions/flux_onetrainer_lora_conversion_utils.py b/invokeai/backend/patches/lora_conversions/flux_onetrainer_lora_conversion_utils.py new file mode 100644 index 00000000000..88aeee95e49 --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/flux_onetrainer_lora_conversion_utils.py @@ -0,0 +1,164 @@ +import re +from typing import Any, Dict + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.utils import any_lora_layer_from_state_dict +from invokeai.backend.patches.lora_conversions.flux_diffusers_lora_conversion_utils import ( + lora_layers_from_flux_diffusers_grouped_state_dict, +) +from invokeai.backend.patches.lora_conversions.flux_kohya_lora_conversion_utils import ( + FLUX_KOHYA_CLIP_KEY_REGEX, + FLUX_KOHYA_T5_KEY_REGEX, + _convert_flux_clip_kohya_state_dict_to_invoke_format, + _convert_flux_t5_kohya_state_dict_to_invoke_format, +) +from invokeai.backend.patches.lora_conversions.flux_lora_constants import ( + FLUX_LORA_CLIP_PREFIX, + FLUX_LORA_T5_PREFIX, +) +from invokeai.backend.patches.lora_conversions.kohya_key_utils import ( + INDEX_PLACEHOLDER, + ParsingTree, + insert_periods_into_kohya_key, +) +from invokeai.backend.patches.model_patch_raw import ModelPatchRaw + +# A regex pattern that matches all of the transformer keys in the OneTrainer FLUX LoRA format. +# The OneTrainer format uses a mix of the Kohya and Diffusers formats: +# - The base model keys are in Diffusers format. +# - Periods are replaced with underscores, to match Kohya. +# - The LoRA key suffixes (e.g. .alpha, .lora_down.weight, .lora_up.weight) match Kohya. +# Example keys: +# - "lora_transformer_single_transformer_blocks_0_attn_to_k.alpha" +# - "lora_transformer_single_transformer_blocks_0_attn_to_k.dora_scale" +# - "lora_transformer_single_transformer_blocks_0_attn_to_k.lora_down.weight" +# - "lora_transformer_single_transformer_blocks_0_attn_to_k.lora_up.weight" +FLUX_ONETRAINER_TRANSFORMER_KEY_REGEX = ( + r"lora_transformer_(single_transformer_blocks|transformer_blocks)_(\d+)_(\w+)\.(.*)" +) + + +def is_state_dict_likely_in_flux_onetrainer_format(state_dict: dict[str | int, Any]) -> bool: + """Checks if the provided state dict is likely in the OneTrainer FLUX LoRA format. + + This is intended to be a high-precision detector, but it is not guaranteed to have perfect precision. (A + perfect-precision detector would require checking all keys against a whitelist and verifying tensor shapes.) + + Note that OneTrainer matches the Kohya format for the CLIP and T5 models. + """ + return all( + re.match(FLUX_ONETRAINER_TRANSFORMER_KEY_REGEX, k) + or re.match(FLUX_KOHYA_CLIP_KEY_REGEX, k) + or re.match(FLUX_KOHYA_T5_KEY_REGEX, k) + for k in state_dict.keys() + if isinstance(k, str) + ) + + +def lora_model_from_flux_onetrainer_state_dict(state_dict: Dict[str, torch.Tensor]) -> ModelPatchRaw: # type: ignore + # Group keys by layer. + grouped_state_dict: dict[str, dict[str, torch.Tensor]] = {} + for key, value in state_dict.items(): + layer_name, param_name = key.split(".", 1) + if layer_name not in grouped_state_dict: + grouped_state_dict[layer_name] = {} + grouped_state_dict[layer_name][param_name] = value + + # Split the grouped state dict into transformer, CLIP, and T5 state dicts. + transformer_grouped_sd: dict[str, dict[str, torch.Tensor]] = {} + clip_grouped_sd: dict[str, dict[str, torch.Tensor]] = {} + t5_grouped_sd: dict[str, dict[str, torch.Tensor]] = {} + for layer_name, layer_state_dict in grouped_state_dict.items(): + if layer_name.startswith("lora_transformer"): + transformer_grouped_sd[layer_name] = layer_state_dict + elif layer_name.startswith("lora_te1"): + clip_grouped_sd[layer_name] = layer_state_dict + elif layer_name.startswith("lora_te2"): + t5_grouped_sd[layer_name] = layer_state_dict + else: + raise ValueError(f"Layer '{layer_name}' does not match the expected pattern for FLUX LoRA weights.") + + # Convert the state dicts to the InvokeAI format. + clip_grouped_sd = _convert_flux_clip_kohya_state_dict_to_invoke_format(clip_grouped_sd) + t5_grouped_sd = _convert_flux_t5_kohya_state_dict_to_invoke_format(t5_grouped_sd) + + # Create LoRA layers. + layers: dict[str, BaseLayerPatch] = {} + for model_prefix, grouped_sd in [ + # (FLUX_LORA_TRANSFORMER_PREFIX, transformer_grouped_sd), + (FLUX_LORA_CLIP_PREFIX, clip_grouped_sd), + (FLUX_LORA_T5_PREFIX, t5_grouped_sd), + ]: + for layer_key, layer_state_dict in grouped_sd.items(): + layers[model_prefix + layer_key] = any_lora_layer_from_state_dict(layer_state_dict) + + # Handle the transformer. + transformer_layers = _convert_flux_transformer_onetrainer_state_dict_to_invoke_format(transformer_grouped_sd) + layers.update(transformer_layers) + + # Create and return the LoRAModelRaw. + return ModelPatchRaw(layers=layers) + + +# This parsing tree was generated by calling `generate_kohya_parsing_tree_from_keys()` on the keys in +# flux_lora_diffusers_format.py. +flux_transformer_kohya_parsing_tree: ParsingTree = { + "transformer": { + "single_transformer_blocks": { + INDEX_PLACEHOLDER: { + "attn": {"to_k": {}, "to_q": {}, "to_v": {}}, + "norm": {"linear": {}}, + "proj_mlp": {}, + "proj_out": {}, + } + }, + "transformer_blocks": { + INDEX_PLACEHOLDER: { + "attn": { + "add_k_proj": {}, + "add_q_proj": {}, + "add_v_proj": {}, + "to_add_out": {}, + "to_k": {}, + "to_out": {INDEX_PLACEHOLDER: {}}, + "to_q": {}, + "to_v": {}, + }, + "ff": {"net": {INDEX_PLACEHOLDER: {"proj": {}}}}, + "ff_context": {"net": {INDEX_PLACEHOLDER: {"proj": {}}}}, + "norm1": {"linear": {}}, + "norm1_context": {"linear": {}}, + } + }, + } +} + + +def _convert_flux_transformer_onetrainer_state_dict_to_invoke_format( + state_dict: Dict[str, Dict[str, torch.Tensor]], +) -> dict[str, BaseLayerPatch]: + """Converts a FLUX transformer LoRA state dict from the OneTrainer FLUX LoRA format to the LoRA weight format used + internally by InvokeAI. + """ + + # Step 1: Convert the Kohya-style keys with underscores to classic keys with periods. + # Example: + # "lora_transformer_single_transformer_blocks_0_attn_to_k.lora_down.weight" -> "transformer.single_transformer_blocks.0.attn.to_k.lora_down.weight" + lora_prefix = "lora_" + lora_prefix_length = len(lora_prefix) + kohya_state_dict: dict[str, Dict[str, torch.Tensor]] = {} + for key in state_dict.keys(): + # Remove the "lora_" prefix. + assert key.startswith(lora_prefix) + new_key = key[lora_prefix_length:] + + # Add periods to the Kohya-style module keys. + new_key = insert_periods_into_kohya_key(new_key, flux_transformer_kohya_parsing_tree) + + # Replace the old key with the new key. + kohya_state_dict[new_key] = state_dict[key] + + # Step 2: Convert diffusers module names to the BFL module names. + return lora_layers_from_flux_diffusers_grouped_state_dict(kohya_state_dict, alpha=None) diff --git a/invokeai/backend/patches/lora_conversions/flux_xlabs_lora_conversion_utils.py b/invokeai/backend/patches/lora_conversions/flux_xlabs_lora_conversion_utils.py new file mode 100644 index 00000000000..b8abbb87635 --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/flux_xlabs_lora_conversion_utils.py @@ -0,0 +1,92 @@ +import re +from typing import Any, Dict + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.utils import any_lora_layer_from_state_dict +from invokeai.backend.patches.lora_conversions.flux_lora_constants import FLUX_LORA_TRANSFORMER_PREFIX +from invokeai.backend.patches.model_patch_raw import ModelPatchRaw + +# A regex pattern that matches all of the transformer keys in the xlabs FLUX LoRA format. +# Example keys: +# double_blocks.0.processor.qkv_lora1.down.weight +# double_blocks.0.processor.qkv_lora1.up.weight +# double_blocks.0.processor.proj_lora1.down.weight +# double_blocks.0.processor.proj_lora1.up.weight +# double_blocks.0.processor.qkv_lora2.down.weight +# double_blocks.0.processor.proj_lora2.up.weight +FLUX_XLABS_KEY_REGEX = r"double_blocks\.(\d+)\.processor\.(qkv|proj)_lora([12])\.(down|up)\.weight" + + +def is_state_dict_likely_in_flux_xlabs_format(state_dict: dict[str | int, Any]) -> bool: + """Checks if the provided state dict is likely in the xlabs FLUX LoRA format. + + The xlabs format is characterized by keys matching the pattern: + double_blocks.{block_idx}.processor.{qkv|proj}_lora{1|2}.{down|up}.weight + + Where: + - lora1 corresponds to the image attention stream (img_attn) + - lora2 corresponds to the text attention stream (txt_attn) + """ + if not state_dict: + return False + + # Check that all keys match the xlabs pattern + for key in state_dict.keys(): + if not isinstance(key, str): + continue + if not re.match(FLUX_XLABS_KEY_REGEX, key): + return False + + # Ensure we have at least some valid keys + return any(isinstance(k, str) and re.match(FLUX_XLABS_KEY_REGEX, k) for k in state_dict.keys()) + + +def lora_model_from_flux_xlabs_state_dict(state_dict: Dict[str, torch.Tensor]) -> ModelPatchRaw: + """Converts an xlabs FLUX LoRA state dict to the InvokeAI ModelPatchRaw format. + + The xlabs format uses: + - lora1 for image attention stream (img_attn) + - lora2 for text attention stream (txt_attn) + - qkv for query/key/value projection + - proj for output projection + + Key mapping: + - double_blocks.X.processor.qkv_lora1 -> double_blocks.X.img_attn.qkv + - double_blocks.X.processor.proj_lora1 -> double_blocks.X.img_attn.proj + - double_blocks.X.processor.qkv_lora2 -> double_blocks.X.txt_attn.qkv + - double_blocks.X.processor.proj_lora2 -> double_blocks.X.txt_attn.proj + """ + # Group keys by layer (without the .down.weight/.up.weight suffix) + grouped_state_dict: dict[str, dict[str, torch.Tensor]] = {} + + for key, value in state_dict.items(): + match = re.match(FLUX_XLABS_KEY_REGEX, key) + if not match: + raise ValueError(f"Key '{key}' does not match the expected pattern for xlabs FLUX LoRA weights.") + + block_idx = match.group(1) + component = match.group(2) # qkv or proj + lora_stream = match.group(3) # 1 or 2 + direction = match.group(4) # down or up + + # Map lora1 -> img_attn, lora2 -> txt_attn + attn_type = "img_attn" if lora_stream == "1" else "txt_attn" + + # Create the InvokeAI-style layer key + layer_key = f"double_blocks.{block_idx}.{attn_type}.{component}" + + if layer_key not in grouped_state_dict: + grouped_state_dict[layer_key] = {} + + # Map down/up to lora_down/lora_up + param_name = f"lora_{direction}.weight" + grouped_state_dict[layer_key][param_name] = value + + # Create LoRA layers + layers: dict[str, BaseLayerPatch] = {} + for layer_key, layer_state_dict in grouped_state_dict.items(): + layers[FLUX_LORA_TRANSFORMER_PREFIX + layer_key] = any_lora_layer_from_state_dict(layer_state_dict) + + return ModelPatchRaw(layers=layers) diff --git a/invokeai/backend/patches/lora_conversions/formats.py b/invokeai/backend/patches/lora_conversions/formats.py new file mode 100644 index 00000000000..b3e00c288bd --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/formats.py @@ -0,0 +1,49 @@ +from typing import Any + +from invokeai.backend.model_manager.taxonomy import FluxLoRAFormat +from invokeai.backend.patches.lora_conversions.flux_aitoolkit_lora_conversion_utils import ( + is_state_dict_likely_in_flux_aitoolkit_format, +) +from invokeai.backend.patches.lora_conversions.flux_bfl_peft_lora_conversion_utils import ( + is_state_dict_likely_in_flux_bfl_peft_format, +) +from invokeai.backend.patches.lora_conversions.flux_control_lora_utils import is_state_dict_likely_flux_control +from invokeai.backend.patches.lora_conversions.flux_diffusers_lora_conversion_utils import ( + is_state_dict_likely_in_flux_diffusers_format, +) +from invokeai.backend.patches.lora_conversions.flux_kohya_lora_conversion_utils import ( + is_state_dict_likely_in_flux_kohya_format, +) +from invokeai.backend.patches.lora_conversions.flux_onetrainer_bfl_lora_conversion_utils import ( + is_state_dict_likely_in_flux_onetrainer_bfl_format, +) +from invokeai.backend.patches.lora_conversions.flux_onetrainer_lora_conversion_utils import ( + is_state_dict_likely_in_flux_onetrainer_format, +) +from invokeai.backend.patches.lora_conversions.flux_xlabs_lora_conversion_utils import ( + is_state_dict_likely_in_flux_xlabs_format, +) + + +def flux_format_from_state_dict( + state_dict: dict[str | int, Any], + metadata: dict[str, Any] | None = None, +) -> FluxLoRAFormat | None: + if is_state_dict_likely_in_flux_kohya_format(state_dict): + return FluxLoRAFormat.Kohya + elif is_state_dict_likely_in_flux_onetrainer_bfl_format(state_dict, metadata): + return FluxLoRAFormat.OneTrainerBfl + elif is_state_dict_likely_in_flux_onetrainer_format(state_dict): + return FluxLoRAFormat.OneTrainer + elif is_state_dict_likely_in_flux_diffusers_format(state_dict): + return FluxLoRAFormat.Diffusers + elif is_state_dict_likely_flux_control(state_dict): + return FluxLoRAFormat.Control + elif is_state_dict_likely_in_flux_aitoolkit_format(state_dict, metadata): + return FluxLoRAFormat.AIToolkit + elif is_state_dict_likely_in_flux_xlabs_format(state_dict): + return FluxLoRAFormat.XLabs + elif is_state_dict_likely_in_flux_bfl_peft_format(state_dict): + return FluxLoRAFormat.BflPeft + else: + return None diff --git a/invokeai/backend/patches/lora_conversions/kohya_key_utils.py b/invokeai/backend/patches/lora_conversions/kohya_key_utils.py new file mode 100644 index 00000000000..42e4c9854fa --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/kohya_key_utils.py @@ -0,0 +1,102 @@ +from typing import Iterable + +INDEX_PLACEHOLDER = "index_placeholder" + + +# Type alias for a 'ParsingTree', which is a recursive dict with string keys. +ParsingTree = dict[str, "ParsingTree"] + + +def insert_periods_into_kohya_key(key: str, parsing_tree: ParsingTree) -> str: + """Insert periods into a Kohya key based on a parsing tree. + + Kohya format keys are produced by replacing periods with underscores in the original key. + + Example: + ``` + key = "module_a_module_b_0_attn_to_k" + parsing_tree = { + "module_a": { + "module_b": { + INDEX_PLACEHOLDER: { + "attn": {}, + }, + }, + }, + } + result = insert_periods_into_kohya_key(key, parsing_tree) + > "module_a.module_b.0.attn.to_k" + ``` + """ + # Split key into parts by underscore. + parts = key.split("_") + + # Build up result by walking through parsing tree and parts. + result_parts: list[str] = [] + current_part = "" + current_tree = parsing_tree + + for part in parts: + if len(current_part) > 0: + current_part = current_part + "_" + current_part += part + + if current_part in current_tree: + # Match found. + current_tree = current_tree[current_part] + result_parts.append(current_part) + current_part = "" + elif current_part.isnumeric() and INDEX_PLACEHOLDER in current_tree: + # Match found with index placeholder. + current_tree = current_tree[INDEX_PLACEHOLDER] + result_parts.append(current_part) + current_part = "" + + if len(current_part) > 0: + raise ValueError(f"Key {key} does not match parsing tree {parsing_tree}.") + + return ".".join(result_parts) + + +def generate_kohya_parsing_tree_from_keys(keys: Iterable[str]) -> ParsingTree: + """Generate a parsing tree from a list of keys. + + Example: + ``` + keys = [ + "module_a.module_b.0.attn.to_k", + "module_a.module_b.1.attn.to_k", + "module_a.module_c.proj", + ] + + tree = generate_kohya_parsing_tree_from_keys(keys) + > { + > "module_a": { + > "module_b": { + > INDEX_PLACEHOLDER: { + > "attn": { + > "to_k": {}, + > "to_q": {}, + > }, + > } + > }, + > "module_c": { + > "proj": {}, + > } + > } + > } + ``` + """ + tree: ParsingTree = {} + for key in keys: + subtree: ParsingTree = tree + for module_name in key.split("."): + key = module_name + if module_name.isnumeric(): + key = INDEX_PLACEHOLDER + + if key not in subtree: + subtree[key] = {} + + subtree = subtree[key] + return tree diff --git a/invokeai/backend/patches/lora_conversions/peft_adapter_utils.py b/invokeai/backend/patches/lora_conversions/peft_adapter_utils.py new file mode 100644 index 00000000000..d680cd0fe2c --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/peft_adapter_utils.py @@ -0,0 +1,72 @@ +"""Utilities for handling PEFT named-adapter LoRA state dicts. + +PEFT (HuggingFace Parameter-Efficient Fine-Tuning) supports multiple named adapters per model. +When saved, the adapter name is encoded in the weight key: + + Standard PEFT: foo.bar.lora_A.weight + Named-adapter PEFT: foo.bar.lora_A..weight + +The most common adapter name is "default", produced automatically by `model.add_adapter()` +without an explicit name. Some training tools (e.g. Diffusers' PEFT integration with +multi-adapter support, certain LoRA fine-tuning scripts) save in this format even with a +single adapter. + +InvokeAI's downstream LoRA detection and conversion code expects the standard PEFT suffix +(`lora_A.weight` / `lora_B.weight`). This module normalizes named-adapter state dicts to +that form so the rest of the pipeline can handle them transparently. +""" + +import re +from typing import Any + +# Match a named-adapter PEFT key ending: .lora_A..weight or .lora_B..weight. +# The adapter name is a single dot-free component (PEFT identifiers do not contain dots). +_NAMED_ADAPTER_RE = re.compile(r"\.lora_([AB])\.([^.]+)\.weight$") + + +def _extract_adapter_names(state_dict: dict[str | int, Any]) -> set[str]: + """Return the set of distinct PEFT adapter names found in the state dict. + + A "named adapter" key is one matching `.lora_A..weight` or `.lora_B..weight`. + Keys in the standard PEFT form (`.lora_A.weight` / `.lora_B.weight`) do not contribute. + """ + names: set[str] = set() + for key in state_dict: + if not isinstance(key, str): + continue + m = _NAMED_ADAPTER_RE.search(key) + if m: + names.add(m.group(2)) + return names + + +def has_peft_named_adapter_keys(state_dict: dict[str | int, Any]) -> bool: + """Check whether the state dict contains any PEFT named-adapter keys.""" + return bool(_extract_adapter_names(state_dict)) + + +def normalize_peft_adapter_names(state_dict: dict[str | int, Any]) -> dict[str | int, Any]: + """Return a state dict with PEFT named-adapter suffixes stripped to the standard form. + + Transforms: + foo.bar.lora_A..weight → foo.bar.lora_A.weight + foo.bar.lora_B..weight → foo.bar.lora_B.weight + + Only applied when the state dict contains exactly one distinct adapter name. If the + file holds multiple adapters, the keys are left untouched (renaming would collide and + multi-adapter LoRAs are not currently supported by InvokeAI). + + If no named-adapter keys are present, the input dict is returned unchanged. + """ + adapter_names = _extract_adapter_names(state_dict) + if len(adapter_names) != 1: + return state_dict + + normalized: dict[str | int, Any] = {} + for key, value in state_dict.items(): + if isinstance(key, str): + new_key = _NAMED_ADAPTER_RE.sub(r".lora_\1.weight", key) + normalized[new_key] = value + else: + normalized[key] = value + return normalized diff --git a/invokeai/backend/patches/lora_conversions/qwen_image_lora_constants.py b/invokeai/backend/patches/lora_conversions/qwen_image_lora_constants.py new file mode 100644 index 00000000000..727ee5a4281 --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/qwen_image_lora_constants.py @@ -0,0 +1,5 @@ +# Qwen Image Edit LoRA prefix constants +# These prefixes are used for key mapping when applying LoRA patches to Qwen Image Edit models + +# Prefix for Qwen Image Edit transformer LoRA layers +QWEN_IMAGE_EDIT_LORA_TRANSFORMER_PREFIX = "lora_transformer-" diff --git a/invokeai/backend/patches/lora_conversions/qwen_image_lora_conversion_utils.py b/invokeai/backend/patches/lora_conversions/qwen_image_lora_conversion_utils.py new file mode 100644 index 00000000000..7fc01f72315 --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/qwen_image_lora_conversion_utils.py @@ -0,0 +1,197 @@ +"""Qwen Image LoRA conversion utilities. + +Qwen Image uses QwenImageTransformer2DModel architecture. +Supports multiple LoRA formats: +- Diffusers/PEFT: transformer_blocks.0.attn.to_k.lora_down.weight +- With prefix: transformer.transformer_blocks.0.attn.to_k.lora_down.weight +- Kohya: lora_unet_transformer_blocks_0_attn_to_k.lora_down.weight (underscores instead of dots) +""" + +import re +from typing import Dict + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.utils import any_lora_layer_from_state_dict +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 + +# Regex for Kohya-format Qwen Image LoRA keys. +# Example: lora_unet_transformer_blocks_0_attn_to_k +# Groups: (block_idx, sub_module_with_underscores) +_KOHYA_KEY_REGEX = re.compile(r"lora_unet_transformer_blocks_(\d+)_(.*)") + +# Mapping from Kohya underscore-separated sub-module names to dot-separated model paths. +# The Kohya format uses underscores everywhere, but some underscores are part of the +# module name (e.g., add_k_proj, to_out). We match the longest prefix first. +_KOHYA_MODULE_MAP: list[tuple[str, str]] = [ + # Attention projections + ("attn_add_k_proj", "attn.add_k_proj"), + ("attn_add_q_proj", "attn.add_q_proj"), + ("attn_add_v_proj", "attn.add_v_proj"), + ("attn_to_add_out", "attn.to_add_out"), + ("attn_to_out_0", "attn.to_out.0"), + ("attn_to_k", "attn.to_k"), + ("attn_to_q", "attn.to_q"), + ("attn_to_v", "attn.to_v"), + # Image stream MLP and modulation + ("img_mlp_net_0_proj", "img_mlp.net.0.proj"), + ("img_mlp_net_2", "img_mlp.net.2"), + ("img_mod_1", "img_mod.1"), + # Text stream MLP and modulation + ("txt_mlp_net_0_proj", "txt_mlp.net.0.proj"), + ("txt_mlp_net_2", "txt_mlp.net.2"), + ("txt_mod_1", "txt_mod.1"), +] + + +def is_state_dict_likely_kohya_qwen_image(state_dict: dict[str | int, torch.Tensor]) -> bool: + """Check if the state dict uses Kohya-format Qwen Image LoRA keys.""" + str_keys = [k for k in state_dict.keys() if isinstance(k, str)] + if not str_keys: + return False + # Check if any key matches the Kohya pattern + return any(k.startswith("lora_unet_transformer_blocks_") for k in str_keys) + + +def _convert_kohya_key(kohya_layer: str) -> str | None: + """Convert a Kohya-format layer name to a dot-separated model module path. + + Example: lora_unet_transformer_blocks_0_attn_to_k -> transformer_blocks.0.attn.to_k + """ + m = _KOHYA_KEY_REGEX.match(kohya_layer) + if not m: + return None + + block_idx = m.group(1) + sub_module = m.group(2) + + for kohya_name, model_path in _KOHYA_MODULE_MAP: + if sub_module == kohya_name: + return f"transformer_blocks.{block_idx}.{model_path}" + + # Fallback: unknown sub-module, return None so caller can warn/skip + return None + + +def lora_model_from_qwen_image_state_dict( + state_dict: Dict[str, torch.Tensor], alpha: float | None = None +) -> ModelPatchRaw: + """Convert a Qwen Image LoRA state dict to a ModelPatchRaw. + + Handles three key formats: + - Diffusers/PEFT: transformer_blocks.0.attn.to_k.lora_down.weight + - With prefix: transformer.transformer_blocks.0.attn.to_k.lora_down.weight + - Kohya: lora_unet_transformer_blocks_0_attn_to_k.lora_down.weight + """ + is_kohya = is_state_dict_likely_kohya_qwen_image(state_dict) + + if is_kohya: + return _convert_kohya_format(state_dict, alpha) + else: + return _convert_diffusers_format(state_dict, alpha) + + +def _convert_kohya_format(state_dict: Dict[str, torch.Tensor], alpha: float | None) -> ModelPatchRaw: + """Convert Kohya-format state dict. Keys are like lora_unet_transformer_blocks_0_attn_to_k.lokr_w1""" + layers: dict[str, BaseLayerPatch] = {} + + # Group by layer (split at first dot: layer_name.param_name) + grouped: dict[str, dict[str, torch.Tensor]] = {} + for key, value in state_dict.items(): + if not isinstance(key, str): + continue + layer_name, param_name = key.split(".", 1) + if layer_name not in grouped: + grouped[layer_name] = {} + grouped[layer_name][param_name] = value + + for kohya_layer, layer_dict in grouped.items(): + model_path = _convert_kohya_key(kohya_layer) + if model_path is None: + continue # Skip unrecognized layers + + layer = any_lora_layer_from_state_dict(layer_dict) + final_key = f"{QWEN_IMAGE_EDIT_LORA_TRANSFORMER_PREFIX}{model_path}" + layers[final_key] = layer + + return ModelPatchRaw(layers=layers) + + +def _convert_diffusers_format(state_dict: Dict[str, torch.Tensor], alpha: float | None) -> ModelPatchRaw: + """Convert Diffusers/PEFT format state dict.""" + layers: dict[str, BaseLayerPatch] = {} + + # Some LoRAs use a "transformer." prefix on keys + strip_prefixes = ["transformer."] + + grouped = _group_by_layer(state_dict) + + for layer_key, layer_dict in grouped.items(): + values = _normalize_lora_keys(layer_dict, alpha) + layer = any_lora_layer_from_state_dict(values) + clean_key = layer_key + for prefix in strip_prefixes: + if clean_key.startswith(prefix): + clean_key = clean_key[len(prefix) :] + break + final_key = f"{QWEN_IMAGE_EDIT_LORA_TRANSFORMER_PREFIX}{clean_key}" + layers[final_key] = layer + + return ModelPatchRaw(layers=layers) + + +def _normalize_lora_keys(layer_dict: dict[str, torch.Tensor], alpha: float | None) -> dict[str, torch.Tensor]: + """Normalize LoRA key names to internal format.""" + if "lora_A.weight" in layer_dict: + values: dict[str, torch.Tensor] = { + "lora_down.weight": layer_dict["lora_A.weight"], + "lora_up.weight": layer_dict["lora_B.weight"], + } + if alpha is not None: + values["alpha"] = torch.tensor(alpha) + return values + elif "lora_down.weight" in layer_dict: + return layer_dict + else: + return layer_dict + + +def _group_by_layer(state_dict: Dict[str, torch.Tensor]) -> dict[str, dict[str, torch.Tensor]]: + """Group state dict keys by layer path.""" + layer_dict: dict[str, dict[str, torch.Tensor]] = {} + + known_suffixes = [ + ".lora_A.weight", + ".lora_B.weight", + ".lora_down.weight", + ".lora_up.weight", + ".dora_scale", + ".alpha", + ] + + for key in state_dict: + if not isinstance(key, str): + continue + + layer_name = None + key_name = None + for suffix in known_suffixes: + if key.endswith(suffix): + layer_name = key[: -len(suffix)] + key_name = suffix[1:] + break + + if layer_name is None: + parts = key.rsplit(".", maxsplit=2) + layer_name = parts[0] + key_name = ".".join(parts[1:]) + + if layer_name not in layer_dict: + layer_dict[layer_name] = {} + layer_dict[layer_name][key_name] = state_dict[key] + + return layer_dict diff --git a/invokeai/backend/patches/lora_conversions/sd_lora_conversion_utils.py b/invokeai/backend/patches/lora_conversions/sd_lora_conversion_utils.py new file mode 100644 index 00000000000..48ea4f91ac7 --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/sd_lora_conversion_utils.py @@ -0,0 +1,29 @@ +from typing import Dict + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.utils import any_lora_layer_from_state_dict +from invokeai.backend.patches.model_patch_raw import ModelPatchRaw + + +def lora_model_from_sd_state_dict(state_dict: Dict[str, torch.Tensor]) -> ModelPatchRaw: + grouped_state_dict: dict[str, dict[str, torch.Tensor]] = _group_state(state_dict) + + layers: dict[str, BaseLayerPatch] = {} + for layer_key, values in grouped_state_dict.items(): + layers[layer_key] = any_lora_layer_from_state_dict(values) + + return ModelPatchRaw(layers=layers) + + +def _group_state(state_dict: Dict[str, torch.Tensor]) -> Dict[str, Dict[str, torch.Tensor]]: + state_dict_groupped: Dict[str, Dict[str, torch.Tensor]] = {} + + for key, value in state_dict.items(): + stem, leaf = key.split(".", 1) + if stem not in state_dict_groupped: + state_dict_groupped[stem] = {} + state_dict_groupped[stem][leaf] = value + + return state_dict_groupped diff --git a/invokeai/backend/patches/lora_conversions/sdxl_lora_conversion_utils.py b/invokeai/backend/patches/lora_conversions/sdxl_lora_conversion_utils.py new file mode 100644 index 00000000000..f96ad5df7cd --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/sdxl_lora_conversion_utils.py @@ -0,0 +1,154 @@ +import bisect +from typing import Dict, List, Tuple, TypeVar + +T = TypeVar("T") + + +def convert_sdxl_keys_to_diffusers_format(state_dict: Dict[str, T]) -> dict[str, T]: + """Convert the keys of an SDXL LoRA state_dict to diffusers format. + + The input state_dict can be in either Stability AI format or diffusers format. If the state_dict is already in + diffusers format, then this function will have no effect. + + This function is adapted from: + https://github.com/bmaltais/kohya_ss/blob/2accb1305979ba62f5077a23aabac23b4c37e935/networks/lora_diffusers.py#L385-L409 + + Args: + state_dict (Dict[str, Tensor]): The SDXL LoRA state_dict. + + Raises: + ValueError: If state_dict contains an unrecognized key, or not all keys could be converted. + + Returns: + Dict[str, Tensor]: The diffusers-format state_dict. + """ + converted_count = 0 # The number of Stability AI keys converted to diffusers format. + not_converted_count = 0 # The number of keys that were not converted. + + # Get a sorted list of Stability AI UNet keys so that we can efficiently search for keys with matching prefixes. + # For example, we want to efficiently find `input_blocks_4_1` in the list when searching for + # `input_blocks_4_1_proj_in`. + stability_unet_keys = list(SDXL_UNET_STABILITY_TO_DIFFUSERS_MAP) + stability_unet_keys.sort() + + new_state_dict: dict[str, T] = {} + for full_key, value in state_dict.items(): + if full_key.startswith("lora_unet_"): + search_key = full_key.replace("lora_unet_", "") + # Use bisect to find the key in stability_unet_keys that *may* match the search_key's prefix. + position = bisect.bisect_right(stability_unet_keys, search_key) + map_key = stability_unet_keys[position - 1] + # Now, check if the map_key *actually* matches the search_key. + if search_key.startswith(map_key): + new_key = full_key.replace(map_key, SDXL_UNET_STABILITY_TO_DIFFUSERS_MAP[map_key]) + new_state_dict[new_key] = value + converted_count += 1 + else: + new_state_dict[full_key] = value + not_converted_count += 1 + elif full_key.startswith("lora_te1_") or full_key.startswith("lora_te2_"): + # The CLIP text encoders have the same keys in both Stability AI and diffusers formats. + new_state_dict[full_key] = value + continue + else: + raise ValueError(f"Unrecognized SDXL LoRA key prefix: '{full_key}'.") + + if converted_count > 0 and not_converted_count > 0: + raise ValueError( + f"The SDXL LoRA could only be partially converted to diffusers format. converted={converted_count}," + f" not_converted={not_converted_count}" + ) + + return new_state_dict + + +# code from +# https://github.com/bmaltais/kohya_ss/blob/2accb1305979ba62f5077a23aabac23b4c37e935/networks/lora_diffusers.py#L15C1-L97C32 +def _make_sdxl_unet_conversion_map() -> List[Tuple[str, str]]: + """Create a dict mapping state_dict keys from Stability AI SDXL format to diffusers SDXL format.""" + unet_conversion_map_layer: list[tuple[str, str]] = [] + + for i in range(3): # num_blocks is 3 in sdxl + # loop over downblocks/upblocks + for j in range(2): + # loop over resnets/attentions for downblocks + hf_down_res_prefix = f"down_blocks.{i}.resnets.{j}." + sd_down_res_prefix = f"input_blocks.{3 * i + j + 1}.0." + unet_conversion_map_layer.append((sd_down_res_prefix, hf_down_res_prefix)) + + if i < 3: + # no attention layers in down_blocks.3 + hf_down_atn_prefix = f"down_blocks.{i}.attentions.{j}." + sd_down_atn_prefix = f"input_blocks.{3 * i + j + 1}.1." + unet_conversion_map_layer.append((sd_down_atn_prefix, hf_down_atn_prefix)) + + for j in range(3): + # loop over resnets/attentions for upblocks + hf_up_res_prefix = f"up_blocks.{i}.resnets.{j}." + sd_up_res_prefix = f"output_blocks.{3 * i + j}.0." + unet_conversion_map_layer.append((sd_up_res_prefix, hf_up_res_prefix)) + + # if i > 0: commentout for sdxl + # no attention layers in up_blocks.0 + hf_up_atn_prefix = f"up_blocks.{i}.attentions.{j}." + sd_up_atn_prefix = f"output_blocks.{3 * i + j}.1." + unet_conversion_map_layer.append((sd_up_atn_prefix, hf_up_atn_prefix)) + + if i < 3: + # no downsample in down_blocks.3 + hf_downsample_prefix = f"down_blocks.{i}.downsamplers.0.conv." + sd_downsample_prefix = f"input_blocks.{3 * (i + 1)}.0.op." + unet_conversion_map_layer.append((sd_downsample_prefix, hf_downsample_prefix)) + + # no upsample in up_blocks.3 + hf_upsample_prefix = f"up_blocks.{i}.upsamplers.0." + sd_upsample_prefix = f"output_blocks.{3 * i + 2}.{2}." # change for sdxl + unet_conversion_map_layer.append((sd_upsample_prefix, hf_upsample_prefix)) + + hf_mid_atn_prefix = "mid_block.attentions.0." + sd_mid_atn_prefix = "middle_block.1." + unet_conversion_map_layer.append((sd_mid_atn_prefix, hf_mid_atn_prefix)) + + for j in range(2): + hf_mid_res_prefix = f"mid_block.resnets.{j}." + sd_mid_res_prefix = f"middle_block.{2 * j}." + unet_conversion_map_layer.append((sd_mid_res_prefix, hf_mid_res_prefix)) + + unet_conversion_map_resnet = [ + # (stable-diffusion, HF Diffusers) + ("in_layers.0.", "norm1."), + ("in_layers.2.", "conv1."), + ("out_layers.0.", "norm2."), + ("out_layers.3.", "conv2."), + ("emb_layers.1.", "time_emb_proj."), + ("skip_connection.", "conv_shortcut."), + ] + + unet_conversion_map: list[tuple[str, str]] = [] + for sd, hf in unet_conversion_map_layer: + if "resnets" in hf: + for sd_res, hf_res in unet_conversion_map_resnet: + unet_conversion_map.append((sd + sd_res, hf + hf_res)) + else: + unet_conversion_map.append((sd, hf)) + + for j in range(2): + hf_time_embed_prefix = f"time_embedding.linear_{j + 1}." + sd_time_embed_prefix = f"time_embed.{j * 2}." + unet_conversion_map.append((sd_time_embed_prefix, hf_time_embed_prefix)) + + for j in range(2): + hf_label_embed_prefix = f"add_embedding.linear_{j + 1}." + sd_label_embed_prefix = f"label_emb.0.{j * 2}." + unet_conversion_map.append((sd_label_embed_prefix, hf_label_embed_prefix)) + + unet_conversion_map.append(("input_blocks.0.0.", "conv_in.")) + unet_conversion_map.append(("out.0.", "conv_norm_out.")) + unet_conversion_map.append(("out.2.", "conv_out.")) + + return unet_conversion_map + + +SDXL_UNET_STABILITY_TO_DIFFUSERS_MAP = { + sd.rstrip(".").replace(".", "_"): hf.rstrip(".").replace(".", "_") for sd, hf in _make_sdxl_unet_conversion_map() +} diff --git a/invokeai/backend/patches/lora_conversions/z_image_lora_constants.py b/invokeai/backend/patches/lora_conversions/z_image_lora_constants.py new file mode 100644 index 00000000000..72d71813153 --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/z_image_lora_constants.py @@ -0,0 +1,8 @@ +# Z-Image LoRA prefix constants +# These prefixes are used for key mapping when applying LoRA patches to Z-Image models + +# Prefix for Z-Image transformer (S3-DiT architecture) LoRA layers +Z_IMAGE_LORA_TRANSFORMER_PREFIX = "lora_transformer-" + +# Prefix for Qwen3 text encoder LoRA layers +Z_IMAGE_LORA_QWEN3_PREFIX = "lora_qwen3-" diff --git a/invokeai/backend/patches/lora_conversions/z_image_lora_conversion_utils.py b/invokeai/backend/patches/lora_conversions/z_image_lora_conversion_utils.py new file mode 100644 index 00000000000..70b10de50d6 --- /dev/null +++ b/invokeai/backend/patches/lora_conversions/z_image_lora_conversion_utils.py @@ -0,0 +1,260 @@ +"""Z-Image LoRA conversion utilities. + +Z-Image uses S3-DiT transformer architecture with Qwen3 text encoder. +LoRAs for Z-Image typically follow the diffusers PEFT format or Kohya format. +""" + +import re +from typing import Any, Dict + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.patches.layers.utils import any_lora_layer_from_state_dict +from invokeai.backend.patches.lora_conversions.z_image_lora_constants import ( + Z_IMAGE_LORA_QWEN3_PREFIX, + Z_IMAGE_LORA_TRANSFORMER_PREFIX, +) +from invokeai.backend.patches.model_patch_raw import ModelPatchRaw + +# Regex for Kohya-format Z-Image transformer keys. +# Example keys: +# lora_unet__layers_0_attention_to_k.alpha +# lora_unet__layers_0_attention_to_k.lora_down.weight +# lora_unet__context_refiner_0_feed_forward_w1.lora_up.weight +# lora_unet__noise_refiner_1_attention_to_v.lora_down.weight +Z_IMAGE_KOHYA_TRANSFORMER_KEY_REGEX = ( + r"lora_unet__(layers|context_refiner|noise_refiner)_(\d+)_(attention|feed_forward)_(to_k|to_q|to_v|w1|w2|w3)" +) + + +def is_state_dict_likely_z_image_kohya_lora(state_dict: dict[str | int, Any]) -> bool: + """Checks if the provided state dict is likely a Z-Image LoRA in Kohya format. + + Kohya Z-Image LoRAs have keys like: + - lora_unet__layers_0_attention_to_k.lora_down.weight + - lora_unet__context_refiner_0_feed_forward_w1.alpha + - lora_unet__noise_refiner_1_attention_to_v.lora_up.weight + """ + return any( + isinstance(k, str) and re.match(Z_IMAGE_KOHYA_TRANSFORMER_KEY_REGEX, k.split(".")[0]) for k in state_dict.keys() + ) + + +def is_state_dict_likely_z_image_lora(state_dict: dict[str | int, torch.Tensor]) -> bool: + """Checks if the provided state dict is likely a Z-Image LoRA. + + Z-Image LoRAs can have keys for transformer and/or Qwen3 text encoder. + They may use various prefixes depending on the training framework. + """ + if is_state_dict_likely_z_image_kohya_lora(state_dict): + return True + + str_keys = [k for k in state_dict.keys() if isinstance(k, str)] + + # Check for Z-Image transformer keys (S3-DiT architecture) + # Various training frameworks use different prefixes + has_transformer_keys = any( + k.startswith( + ( + "transformer.", + "base_model.model.transformer.", + "diffusion_model.", + ) + ) + for k in str_keys + ) + + # Check for Qwen3 text encoder keys + has_qwen3_keys = any(k.startswith(("text_encoder.", "base_model.model.text_encoder.")) for k in str_keys) + + return has_transformer_keys or has_qwen3_keys + + +def lora_model_from_z_image_state_dict( + state_dict: Dict[str, torch.Tensor], alpha: float | None = None +) -> ModelPatchRaw: + """Convert a Z-Image LoRA state dict to a ModelPatchRaw. + + Z-Image LoRAs can contain layers for: + - Transformer (S3-DiT architecture) + - Qwen3 text encoder + + Z-Image LoRAs may use various key prefixes depending on how they were trained: + - "transformer." or "base_model.model.transformer." for diffusers PEFT format + - "diffusion_model." for some training frameworks + - "text_encoder." or "base_model.model.text_encoder." for Qwen3 encoder + - "lora_unet__" for Kohya format (underscores instead of dots) + + Args: + state_dict: The LoRA state dict + alpha: The alpha value for LoRA scaling. If None, uses rank as alpha. + + Returns: + A ModelPatchRaw containing the LoRA layers + """ + # If Kohya format, convert keys first then process normally + if is_state_dict_likely_z_image_kohya_lora(state_dict): + state_dict = _convert_z_image_kohya_state_dict(state_dict) + + layers: dict[str, BaseLayerPatch] = {} + + # Group keys by layer + grouped_state_dict = _group_by_layer(state_dict) + + for layer_key, layer_dict in grouped_state_dict.items(): + # Convert PEFT format keys to internal format + values = _get_lora_layer_values(layer_dict, alpha) + + # Determine the appropriate prefix based on the layer type and clean up the key + clean_key = layer_key + + # Handle various transformer prefixes + transformer_prefixes = [ + "base_model.model.transformer.diffusion_model.", + "base_model.model.transformer.", + "transformer.diffusion_model.", + "transformer.", + "diffusion_model.", + ] + + # Handle text encoder prefixes + text_encoder_prefixes = [ + "base_model.model.text_encoder.", + "text_encoder.", + ] + + is_text_encoder = False + + # Check and strip text encoder prefixes first + for prefix in text_encoder_prefixes: + if layer_key.startswith(prefix): + clean_key = layer_key[len(prefix) :] + is_text_encoder = True + break + + # If not text encoder, check transformer prefixes + if not is_text_encoder: + for prefix in transformer_prefixes: + if layer_key.startswith(prefix): + clean_key = layer_key[len(prefix) :] + break + + # Apply the appropriate internal prefix + if is_text_encoder: + final_key = f"{Z_IMAGE_LORA_QWEN3_PREFIX}{clean_key}" + else: + final_key = f"{Z_IMAGE_LORA_TRANSFORMER_PREFIX}{clean_key}" + + layer = any_lora_layer_from_state_dict(values) + layers[final_key] = layer + + return ModelPatchRaw(layers=layers) + + +def _convert_z_image_kohya_state_dict(state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]: + """Converts a Kohya-format Z-Image LoRA state dict to diffusion_model dot-notation. + + Example key conversions: + - lora_unet__layers_0_attention_to_k.lora_down.weight -> diffusion_model.layers.0.attention.to_k.lora_down.weight + - lora_unet__context_refiner_0_feed_forward_w1.alpha -> diffusion_model.context_refiner.0.feed_forward.w1.alpha + - lora_unet__noise_refiner_1_attention_to_v.lora_up.weight -> diffusion_model.noise_refiner.1.attention.to_v.lora_up.weight + """ + converted: Dict[str, torch.Tensor] = {} + for key, value in state_dict.items(): + if not isinstance(key, str) or not key.startswith("lora_unet__"): + converted[key] = value + continue + + # Split into layer name and param suffix (e.g. "lora_down.weight", "alpha") + layer_name, _, param_suffix = key.partition(".") + + # Strip lora_unet__ prefix + remainder = layer_name[len("lora_unet__") :] + + # Convert Kohya underscore format to dot-notation using the known structure + match = re.match( + r"(layers|context_refiner|noise_refiner)_(\d+)_(attention|feed_forward)_(to_k|to_q|to_v|w1|w2|w3)$", + remainder, + ) + if match: + block, idx, submodule, param = match.groups() + new_layer = f"diffusion_model.{block}.{idx}.{submodule}.{param}" + else: + # Fallback: keep original key for unrecognized patterns + converted[key] = value + continue + + new_key = f"{new_layer}.{param_suffix}" if param_suffix else new_layer + converted[new_key] = value + + return converted + + +def _get_lora_layer_values(layer_dict: dict[str, torch.Tensor], alpha: float | None) -> dict[str, torch.Tensor]: + """Convert layer dict keys from PEFT format to internal format.""" + if "lora_A.weight" in layer_dict: + # PEFT format: lora_A.weight, lora_B.weight + values = { + "lora_down.weight": layer_dict["lora_A.weight"], + "lora_up.weight": layer_dict["lora_B.weight"], + } + if alpha is not None: + values["alpha"] = torch.tensor(alpha) + return values + elif "lora_down.weight" in layer_dict: + # Already in internal format + return layer_dict + else: + # Unknown format, return as-is + return layer_dict + + +def _group_by_layer(state_dict: Dict[str, torch.Tensor]) -> dict[str, dict[str, torch.Tensor]]: + """Groups the keys in the state dict by layer. + + Z-Image LoRAs have keys like: + - diffusion_model.layers.17.attention.to_k.alpha + - diffusion_model.layers.17.attention.to_k.dora_scale + - diffusion_model.layers.17.attention.to_k.lora_down.weight + - diffusion_model.layers.17.attention.to_k.lora_up.weight + + We need to group these by the full layer path (e.g., diffusion_model.layers.17.attention.to_k) + and extract the suffix (alpha, dora_scale, lora_down.weight, lora_up.weight). + """ + layer_dict: dict[str, dict[str, torch.Tensor]] = {} + + # Known suffixes that indicate the end of a layer name + known_suffixes = [ + ".lora_A.weight", + ".lora_B.weight", + ".lora_down.weight", + ".lora_up.weight", + ".dora_scale", + ".alpha", + ] + + for key in state_dict: + if not isinstance(key, str): + continue + + # Try to find a known suffix + layer_name = None + key_name = None + for suffix in known_suffixes: + if key.endswith(suffix): + layer_name = key[: -len(suffix)] + key_name = suffix[1:] # Remove leading dot + break + + if layer_name is None: + # Fallback to original logic for unknown formats + parts = key.rsplit(".", maxsplit=2) + layer_name = parts[0] + key_name = ".".join(parts[1:]) + + if layer_name not in layer_dict: + layer_dict[layer_name] = {} + layer_dict[layer_name][key_name] = state_dict[key] + + return layer_dict diff --git a/invokeai/backend/patches/model_patch_raw.py b/invokeai/backend/patches/model_patch_raw.py new file mode 100644 index 00000000000..439ee9b9100 --- /dev/null +++ b/invokeai/backend/patches/model_patch_raw.py @@ -0,0 +1,19 @@ +# Copyright (c) 2024 The InvokeAI Development team +from typing import Mapping, Optional + +import torch + +from invokeai.backend.patches.layers.base_layer_patch import BaseLayerPatch +from invokeai.backend.raw_model import RawModel + + +class ModelPatchRaw(RawModel): + def __init__(self, layers: Mapping[str, BaseLayerPatch]): + self.layers = layers + + def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None: + for layer in self.layers.values(): + layer.to(device=device, dtype=dtype) + + def calc_size(self) -> int: + return sum(layer.calc_size() for layer in self.layers.values()) diff --git a/invokeai/backend/patches/pad_with_zeros.py b/invokeai/backend/patches/pad_with_zeros.py new file mode 100644 index 00000000000..a76b02f0b36 --- /dev/null +++ b/invokeai/backend/patches/pad_with_zeros.py @@ -0,0 +1,9 @@ +import torch + + +def pad_with_zeros(orig_weight: torch.Tensor, target_shape: torch.Size) -> torch.Tensor: + """Pad a weight tensor with zeros to match the target shape.""" + expanded_weight = torch.zeros(target_shape, dtype=orig_weight.dtype, device=orig_weight.device) + slices = tuple(slice(0, dim) for dim in orig_weight.shape) + expanded_weight[slices] = orig_weight + return expanded_weight diff --git a/invokeai/backend/quantization/__init__.py b/invokeai/backend/quantization/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/quantization/bnb_llm_int8.py b/invokeai/backend/quantization/bnb_llm_int8.py new file mode 100644 index 00000000000..8722a19c373 --- /dev/null +++ b/invokeai/backend/quantization/bnb_llm_int8.py @@ -0,0 +1,154 @@ +import bitsandbytes as bnb +import torch + +# This file contains utils for working with models that use bitsandbytes LLM.int8() quantization. +# The utils in this file are partially inspired by: +# https://github.com/Lightning-AI/pytorch-lightning/blob/1551a16b94f5234a4a78801098f64d0732ef5cb5/src/lightning/fabric/plugins/precision/bitsandbytes.py + + +# NOTE(ryand): All of the custom state_dict manipulation logic in this file is pretty hacky. This could be made much +# cleaner by re-implementing bnb.nn.Linear8bitLt with proper use of buffers and less magic. But, for now, we try to +# stick close to the bitsandbytes classes to make interoperability easier with other models that might use bitsandbytes. + + +class InvokeInt8Params(bnb.nn.Int8Params): + """We override cuda() to avoid re-quantizing the weights in the following cases: + - We loaded quantized weights from a state_dict on the cpu, and then moved the model to the gpu. + - We are moving the model back-and-forth between the cpu and gpu. + """ + + def cuda(self, device): + if self.has_fp16_weights: + return super().cuda(device) + elif self.CB is not None and self.SCB is not None: + self.data = self.data.cuda() + self.CB = self.data + self.SCB = self.SCB.cuda() + else: + # We quantize the weight and store in 8bit row-major + B = self.data.contiguous().half().cuda(device) + CB, SCB, _ = bnb.functional.int8_vectorwise_quant(B) + self.data = CB + self.CB = CB + self.SCB = SCB + + return self + + +class InvokeLinear8bitLt(bnb.nn.Linear8bitLt): + def _load_from_state_dict( + self, + state_dict: dict[str, torch.Tensor], + prefix: str, + local_metadata, + strict, + missing_keys, + unexpected_keys, + error_msgs, + ): + weight = state_dict.pop(prefix + "weight") + bias = state_dict.pop(prefix + "bias", None) + + # See `bnb.nn.Linear8bitLt._save_to_state_dict()` for the serialization logic of SCB and weight_format. + scb = state_dict.pop(prefix + "SCB", None) + + weight_format = state_dict.pop(prefix + "weight_format", None) + if weight_format is not None: + # Currently, we only support weight_format=0. + assert weight_format == 0 + + # TODO(ryand): Technically, we should be using `strict`, `missing_keys`, `unexpected_keys`, and `error_msgs` + # rather than raising an exception to correctly implement this API. + assert len(state_dict) == 0 + + if scb is not None: + # We are loading a pre-quantized state dict. + self.weight = InvokeInt8Params( + data=weight, + requires_grad=self.weight.requires_grad, + has_fp16_weights=False, + # Note: After quantization, CB is the same as weight. + CB=weight, + SCB=scb, + ) + self.bias = bias if bias is None else torch.nn.Parameter(bias) + else: + # We are loading a non-quantized state dict. + + # We could simply call the `super()._load_from_state_dict()` method here, but then we wouldn't be able to + # load from a state_dict into a model on the "meta" device. Attempting to load into a model on the "meta" + # device requires setting `assign=True`, doing this with the default `super()._load_from_state_dict()` + # implementation causes `Params4Bit` to be replaced by a `torch.nn.Parameter`. By initializing a new + # `Params4bit` object, we work around this issue. It's a bit hacky, but it gets the job done. + self.weight = InvokeInt8Params( + data=weight, + requires_grad=self.weight.requires_grad, + has_fp16_weights=False, + CB=None, + SCB=None, + ) + self.bias = bias if bias is None else torch.nn.Parameter(bias) + + # Reset the state. The persisted fields are based on the initialization behaviour in + # `bnb.nn.Linear8bitLt.__init__()`. + new_state = bnb.MatmulLtState() + new_state.threshold = self.state.threshold + new_state.has_fp16_weights = False + new_state.use_pool = self.state.use_pool + self.state = new_state + + def forward(self, x: torch.Tensor): + # The state management in the base bnb.nn.Linear8bitLt is very convoluted. We override the forward method to + # try to simplify the state management a bit. We initialize a new MatmulLtState object for each forward pass. + # By avoiding persistent state, it is easier to move the layer between devices without worrying about keeping + # references to weights on the old device (e.g. self.state.CB). + matmul_state = bnb.MatmulLtState() + matmul_state.threshold = self.state.threshold + matmul_state.has_fp16_weights = self.state.has_fp16_weights + matmul_state.use_pool = self.state.use_pool + matmul_state.is_training = self.training + # The underlying InvokeInt8Params weight must already be quantized. + assert self.weight.CB is not None + matmul_state.CB = self.weight.CB + matmul_state.SCB = self.weight.SCB + + # weights are cast automatically as Int8Params, but the bias has to be cast manually. + if self.bias is not None and self.bias.dtype != x.dtype: + self.bias.data = self.bias.data.to(x.dtype) + + return bnb.matmul(x, self.weight, bias=self.bias, state=matmul_state) + + +def _convert_linear_layers_to_llm_8bit( + module: torch.nn.Module, ignore_modules: set[str], outlier_threshold: float, prefix: str = "" +) -> None: + """Convert all linear layers in the module to bnb.nn.Linear8bitLt layers.""" + for name, child in module.named_children(): + fullname = f"{prefix}.{name}" if prefix else name + if isinstance(child, torch.nn.Linear) and not any(fullname.startswith(s) for s in ignore_modules): + has_bias = child.bias is not None + replacement = InvokeLinear8bitLt( + child.in_features, + child.out_features, + bias=has_bias, + has_fp16_weights=False, + threshold=outlier_threshold, + ) + replacement.weight.data = child.weight.data + if has_bias: + replacement.bias.data = child.bias.data + replacement.requires_grad_(False) + module.__setattr__(name, replacement) + else: + _convert_linear_layers_to_llm_8bit( + child, ignore_modules, outlier_threshold=outlier_threshold, prefix=fullname + ) + + +def quantize_model_llm_int8(model: torch.nn.Module, modules_to_not_convert: set[str], outlier_threshold: float = 6.0): + """Apply bitsandbytes LLM.8bit() quantization to the model.""" + _convert_linear_layers_to_llm_8bit( + module=model, ignore_modules=modules_to_not_convert, outlier_threshold=outlier_threshold + ) + + return model diff --git a/invokeai/backend/quantization/bnb_nf4.py b/invokeai/backend/quantization/bnb_nf4.py new file mode 100644 index 00000000000..105bf1474c1 --- /dev/null +++ b/invokeai/backend/quantization/bnb_nf4.py @@ -0,0 +1,156 @@ +import bitsandbytes as bnb +import torch + +# This file contains utils for working with models that use bitsandbytes NF4 quantization. +# The utils in this file are partially inspired by: +# https://github.com/Lightning-AI/pytorch-lightning/blob/1551a16b94f5234a4a78801098f64d0732ef5cb5/src/lightning/fabric/plugins/precision/bitsandbytes.py + +# NOTE(ryand): All of the custom state_dict manipulation logic in this file is pretty hacky. This could be made much +# cleaner by re-implementing bnb.nn.LinearNF4 with proper use of buffers and less magic. But, for now, we try to stick +# close to the bitsandbytes classes to make interoperability easier with other models that might use bitsandbytes. + + +class InvokeLinearNF4(bnb.nn.LinearNF4): + """A class that extends `bnb.nn.LinearNF4` to add the following functionality: + - Ability to load Linear NF4 layers from a pre-quantized state_dict. + - Ability to load Linear NF4 layers from a state_dict when the model is on the "meta" device. + """ + + def _load_from_state_dict( + self, + state_dict: dict[str, torch.Tensor], + prefix: str, + local_metadata, + strict, + missing_keys, + unexpected_keys, + error_msgs, + ): + """This method is based on the logic in the bitsandbytes serialization unit tests for `Linear4bit`: + https://github.com/bitsandbytes-foundation/bitsandbytes/blob/6d714a5cce3db5bd7f577bc447becc7a92d5ccc7/tests/test_linear4bit.py#L52-L71 + """ + weight = state_dict.pop(prefix + "weight") + bias = state_dict.pop(prefix + "bias", None) + # We expect the remaining keys to be quant_state keys. + quant_state_sd = state_dict + + # During serialization, the quant_state is stored as subkeys of "weight." (See + # `bnb.nn.LinearNF4._save_to_state_dict()`). We validate that they at least have the correct prefix. + # TODO(ryand): Technically, we should be using `strict`, `missing_keys`, `unexpected_keys`, and `error_msgs` + # rather than raising an exception to correctly implement this API. + assert all(k.startswith(prefix + "weight.") for k in quant_state_sd.keys()) + + if len(quant_state_sd) > 0: + # We are loading a pre-quantized state dict. + self.weight = bnb.nn.Params4bit.from_prequantized( + data=weight, quantized_stats=quant_state_sd, device=weight.device + ) + self.bias = bias if bias is None else torch.nn.Parameter(bias, requires_grad=False) + else: + # We are loading a non-quantized state dict. + + # We could simply call the `super()._load_from_state_dict()` method here, but then we wouldn't be able to + # load from a state_dict into a model on the "meta" device. Attempting to load into a model on the "meta" + # device requires setting `assign=True`, doing this with the default `super()._load_from_state_dict()` + # implementation causes `Params4Bit` to be replaced by a `torch.nn.Parameter`. By initializing a new + # `Params4bit` object, we work around this issue. It's a bit hacky, but it gets the job done. + self.weight = bnb.nn.Params4bit( + data=weight, + requires_grad=self.weight.requires_grad, + compress_statistics=self.weight.compress_statistics, + quant_type=self.weight.quant_type, + quant_storage=self.weight.quant_storage, + module=self, + ) + self.bias = bias if bias is None else torch.nn.Parameter(bias) + + +def _replace_param( + param: torch.nn.Parameter | bnb.nn.Params4bit, + data: torch.Tensor, +) -> torch.nn.Parameter: + """A helper function to replace the data of a model parameter with new data in a way that allows replacing params on + the "meta" device. + + Supports both `torch.nn.Parameter` and `bnb.nn.Params4bit` parameters. + """ + if param.device.type == "meta": + # Doing `param.data = data` raises a RuntimeError if param.data was on the "meta" device, so we need to + # re-create the param instead of overwriting the data. + if isinstance(param, bnb.nn.Params4bit): + return bnb.nn.Params4bit( + data, + requires_grad=data.requires_grad, + quant_state=param.quant_state, + compress_statistics=param.compress_statistics, + quant_type=param.quant_type, + ) + return torch.nn.Parameter(data, requires_grad=data.requires_grad) + + param.data = data + return param + + +def _convert_linear_layers_to_nf4( + module: torch.nn.Module, + ignore_modules: set[str], + compute_dtype: torch.dtype, + compress_statistics: bool = False, + prefix: str = "", +) -> None: + """Convert all linear layers in the model to NF4 quantized linear layers. + + Args: + module: All linear layers in this module will be converted. + ignore_modules: A set of module prefixes to ignore when converting linear layers. + compute_dtype: The dtype to use for computation in the quantized linear layers. + compress_statistics: Whether to enable nested quantization (aka double quantization) where the quantization + constants from the first quantization are quantized again. + prefix: The prefix of the current module in the model. Used to call this function recursively. + """ + for name, child in module.named_children(): + fullname = f"{prefix}.{name}" if prefix else name + if isinstance(child, torch.nn.Linear) and not any(fullname.startswith(s) for s in ignore_modules): + has_bias = child.bias is not None + replacement = InvokeLinearNF4( + child.in_features, + child.out_features, + bias=has_bias, + compute_dtype=compute_dtype, + compress_statistics=compress_statistics, + ) + if has_bias: + replacement.bias = _replace_param(replacement.bias, child.bias.data) + replacement.weight = _replace_param(replacement.weight, child.weight.data) + replacement.requires_grad_(False) + module.__setattr__(name, replacement) + else: + _convert_linear_layers_to_nf4(child, ignore_modules, compute_dtype=compute_dtype, prefix=fullname) + + +def quantize_model_nf4(model: torch.nn.Module, modules_to_not_convert: set[str], compute_dtype: torch.dtype): + """Apply bitsandbytes nf4 quantization to the model. + + You likely want to call this function inside a `accelerate.init_empty_weights()` context. + + Example usage: + ``` + # Initialize the model from a config on the meta device. + with accelerate.init_empty_weights(): + model = ModelClass.from_config(...) + + # Add NF4 quantization linear layers to the model - still on the meta device. + with accelerate.init_empty_weights(): + model = quantize_model_nf4(model, modules_to_not_convert=set(), compute_dtype=torch.float16) + + # Load a state_dict into the model. (Could be either a prequantized or non-quantized state_dict.) + model.load_state_dict(state_dict, strict=True, assign=True) + + # Move the model to the "cuda" device. If the model was non-quantized, this is where the weight quantization takes + # place. + model.to("cuda") + ``` + """ + _convert_linear_layers_to_nf4(module=model, ignore_modules=modules_to_not_convert, compute_dtype=compute_dtype) + + return model diff --git a/invokeai/backend/quantization/gguf/ggml_tensor.py b/invokeai/backend/quantization/gguf/ggml_tensor.py new file mode 100644 index 00000000000..af895fb3eee --- /dev/null +++ b/invokeai/backend/quantization/gguf/ggml_tensor.py @@ -0,0 +1,196 @@ +from typing import overload + +import gguf +import torch + +from invokeai.backend.quantization.gguf.utils import ( + DEQUANTIZE_FUNCTIONS, + TORCH_COMPATIBLE_QTYPES, + dequantize, +) + + +def dequantize_and_run(func, args, kwargs): + """A helper function for running math ops on GGMLTensor inputs. + + Dequantizes the inputs, and runs the function. + Also casts other floating point tensors to match the compute_dtype of GGMLTensors + to avoid dtype mismatches in matrix operations. + """ + # Find the compute_dtype and target_device from any GGMLTensor in the args + compute_dtype = None + target_device = None + for a in args: + if hasattr(a, "compute_dtype"): + compute_dtype = a.compute_dtype + if isinstance(a, torch.Tensor) and target_device is None: + target_device = a.device + if compute_dtype is not None and target_device is not None: + break + if compute_dtype is None or target_device is None: + for v in kwargs.values(): + if hasattr(v, "compute_dtype") and compute_dtype is None: + compute_dtype = v.compute_dtype + if isinstance(v, torch.Tensor) and target_device is None: + target_device = v.device + if compute_dtype is not None and target_device is not None: + break + + def process_tensor(t): + if hasattr(t, "get_dequantized_tensor"): + result = t.get_dequantized_tensor() + # Ensure the dequantized tensor is on the target device + if target_device is not None and result.device != target_device: + result = result.to(target_device) + return result + elif isinstance(t, torch.Tensor) and compute_dtype is not None and t.is_floating_point(): + # Cast other floating point tensors to match the GGUF compute_dtype + return t.to(compute_dtype) + return t + + dequantized_args = [process_tensor(a) for a in args] + dequantized_kwargs = {k: process_tensor(v) for k, v in kwargs.items()} + return func(*dequantized_args, **dequantized_kwargs) + + +def apply_to_quantized_tensor(func, args, kwargs): + """A helper function to apply a function to a quantized GGML tensor, and re-wrap the result in a GGMLTensor. + + Assumes that the first argument is a GGMLTensor. + """ + # We expect the first argument to be a GGMLTensor, and all other arguments to be non-GGMLTensors. + ggml_tensor = args[0] + assert isinstance(ggml_tensor, GGMLTensor) + assert all(not isinstance(a, GGMLTensor) for a in args[1:]) + assert all(not isinstance(v, GGMLTensor) for v in kwargs.values()) + + new_data = func(ggml_tensor.quantized_data, *args[1:], **kwargs) + + if new_data.dtype != ggml_tensor.quantized_data.dtype: + # This is intended to catch calls such as `.to(dtype-torch.float32)`, which are not supported on GGMLTensors. + raise ValueError("Operation changed the dtype of GGMLTensor unexpectedly.") + + return GGMLTensor( + new_data, ggml_tensor._ggml_quantization_type, ggml_tensor.tensor_shape, ggml_tensor.compute_dtype + ) + + +GGML_TENSOR_OP_TABLE = { + # Ops to run on the quantized tensor. + torch.ops.aten.detach.default: apply_to_quantized_tensor, # pyright: ignore + torch.ops.aten._to_copy.default: apply_to_quantized_tensor, # pyright: ignore + torch.ops.aten.clone.default: apply_to_quantized_tensor, # pyright: ignore + # Ops to run on dequantized tensors. + torch.ops.aten.t.default: dequantize_and_run, # pyright: ignore + torch.ops.aten.addmm.default: dequantize_and_run, # pyright: ignore + torch.ops.aten.mul.Tensor: dequantize_and_run, # pyright: ignore + torch.ops.aten.add.Tensor: dequantize_and_run, # pyright: ignore + torch.ops.aten.sub.Tensor: dequantize_and_run, # pyright: ignore + torch.ops.aten.allclose.default: dequantize_and_run, # pyright: ignore + torch.ops.aten.slice.Tensor: dequantize_and_run, # pyright: ignore + torch.ops.aten.view.default: dequantize_and_run, # pyright: ignore + torch.ops.aten.expand.default: dequantize_and_run, # pyright: ignore + torch.ops.aten.index_put_.default: dequantize_and_run, # pyright: ignore +} + +if torch.backends.mps.is_available(): + GGML_TENSOR_OP_TABLE.update( + {torch.ops.aten.linear.default: dequantize_and_run} # pyright: ignore + ) + + +class GGMLTensor(torch.Tensor): + """A torch.Tensor sub-class holding a quantized GGML tensor. + + The underlying tensor is quantized, but the GGMLTensor class provides a dequantized view of the tensor on-the-fly + when it is used in operations. + """ + + @staticmethod + def __new__( + cls, + data: torch.Tensor, + ggml_quantization_type: gguf.GGMLQuantizationType, + tensor_shape: torch.Size, + compute_dtype: torch.dtype, + ): + # Type hinting is not supported for torch.Tensor._make_wrapper_subclass, so we ignore the errors. + return torch.Tensor._make_wrapper_subclass( # pyright: ignore + cls, + data.shape, + dtype=data.dtype, + layout=data.layout, + device=data.device, + strides=data.stride(), + storage_offset=data.storage_offset(), + ) + + def __init__( + self, + data: torch.Tensor, + ggml_quantization_type: gguf.GGMLQuantizationType, + tensor_shape: torch.Size, + compute_dtype: torch.dtype, + ): + self.quantized_data = data + self._ggml_quantization_type = ggml_quantization_type + # The dequantized shape of the tensor. + self.tensor_shape = tensor_shape + self.compute_dtype = compute_dtype + + def __repr__(self, *, tensor_contents=None): + return f"GGMLTensor(type={self._ggml_quantization_type.name}, dequantized_shape=({self.tensor_shape})" + + @overload + def size(self, dim: None = None) -> torch.Size: ... + + @overload + def size(self, dim: int) -> int: ... + + def size(self, dim: int | None = None): + """Return the size of the tensor after dequantization. I.e. the shape that will be used in any math ops.""" + if dim is not None: + return self.tensor_shape[dim] + return self.tensor_shape + + @property + def shape(self) -> torch.Size: # pyright: ignore[reportIncompatibleVariableOverride] pyright doesn't understand this for some reason. + """The shape of the tensor after dequantization. I.e. the shape that will be used in any math ops.""" + return self.size() + + @property + def quantized_shape(self) -> torch.Size: + """The shape of the quantized tensor.""" + return self.quantized_data.shape + + def requires_grad_(self, mode: bool = True) -> torch.Tensor: + """The GGMLTensor class is currently only designed for inference (not training). Setting requires_grad to True + is not supported. This method is a no-op. + """ + return self + + def get_dequantized_tensor(self): + """Return the dequantized tensor. + + Args: + dtype: The dtype of the dequantized tensor. + """ + if self._ggml_quantization_type in TORCH_COMPATIBLE_QTYPES: + return self.quantized_data.to(self.compute_dtype) + elif self._ggml_quantization_type in DEQUANTIZE_FUNCTIONS: + # TODO(ryand): Look into how the dtype param is intended to be used. + return dequantize( + data=self.quantized_data, qtype=self._ggml_quantization_type, oshape=self.tensor_shape, dtype=None + ).to(self.compute_dtype) + else: + # There is no GPU implementation for this quantization type, so fallback to the numpy implementation. + new = gguf.quants.dequantize(self.quantized_data.cpu().numpy(), self._ggml_quantization_type) + return torch.from_numpy(new).to(self.quantized_data.device, dtype=self.compute_dtype) + + @classmethod + def __torch_dispatch__(cls, func, types, args, kwargs): + # We will likely hit cases here in the future where a new op is encountered that is not yet supported. + # The new op simply needs to be added to the GGML_TENSOR_OP_TABLE. + if func in GGML_TENSOR_OP_TABLE: + return GGML_TENSOR_OP_TABLE[func](func, args, kwargs) + return NotImplemented diff --git a/invokeai/backend/quantization/gguf/loaders.py b/invokeai/backend/quantization/gguf/loaders.py new file mode 100644 index 00000000000..cb8ac2dbeb6 --- /dev/null +++ b/invokeai/backend/quantization/gguf/loaders.py @@ -0,0 +1,57 @@ +import gc +from pathlib import Path + +import gguf +import torch + +from invokeai.backend.quantization.gguf.ggml_tensor import GGMLTensor +from invokeai.backend.quantization.gguf.utils import TORCH_COMPATIBLE_QTYPES +from invokeai.backend.util.logging import InvokeAILogger + +logger = InvokeAILogger.get_logger() + + +class WrappedGGUFReader: + """Wrapper around GGUFReader that adds a close() method.""" + + def __init__(self, path: Path): + self.reader = gguf.GGUFReader(path) + + def __enter__(self): + return self.reader + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + return False + + def close(self): + """Explicitly close the memory-mapped file.""" + if hasattr(self.reader, "data"): + try: + self.reader.data.flush() + del self.reader.data + except (AttributeError, OSError, ValueError) as e: + logger.warning(f"Wasn't able to close GGUF memory map: {e}") + del self.reader + gc.collect() + + +def gguf_sd_loader(path: Path, compute_dtype: torch.dtype) -> dict[str, GGMLTensor]: + with WrappedGGUFReader(path) as reader: + sd: dict[str, GGMLTensor] = {} + for tensor in reader.tensors: + # Use .copy() to create a true copy of the data, not a view. + # This is critical on Windows where the memory-mapped file cannot be deleted + # while tensors still hold references to the mapped memory. + torch_tensor = torch.from_numpy(tensor.data.copy()) + + shape = torch.Size(tuple(int(v) for v in reversed(tensor.shape))) + if tensor.tensor_type in TORCH_COMPATIBLE_QTYPES: + torch_tensor = torch_tensor.view(*shape) + sd[tensor.name] = GGMLTensor( + torch_tensor, + ggml_quantization_type=tensor.tensor_type, + tensor_shape=shape, + compute_dtype=compute_dtype, + ) + return sd diff --git a/invokeai/backend/quantization/gguf/utils.py b/invokeai/backend/quantization/gguf/utils.py new file mode 100644 index 00000000000..78e9fbfebe2 --- /dev/null +++ b/invokeai/backend/quantization/gguf/utils.py @@ -0,0 +1,309 @@ +# Largely based on https://github.com/city96/ComfyUI-GGUF + +from typing import Callable, Optional, Union + +import gguf +import torch + +# should not be a Set until this is resolved: https://github.com/pytorch/pytorch/issues/145761 +TORCH_COMPATIBLE_QTYPES = [None, gguf.GGMLQuantizationType.F32, gguf.GGMLQuantizationType.F16] + +# K Quants # +QK_K = 256 +K_SCALE_SIZE = 12 + + +def get_scale_min(scales: torch.Tensor): + n_blocks = scales.shape[0] + scales = scales.view(torch.uint8) + scales = scales.reshape((n_blocks, 3, 4)) + + d, m, m_d = torch.split(scales, scales.shape[-2] // 3, dim=-2) + + sc = torch.cat([d & 0x3F, (m_d & 0x0F) | ((d >> 2) & 0x30)], dim=-1) + min = torch.cat([m & 0x3F, (m_d >> 4) | ((m >> 2) & 0x30)], dim=-1) + + return (sc.reshape((n_blocks, 8)), min.reshape((n_blocks, 8))) + + +# Legacy Quants # +def dequantize_blocks_Q8_0( + blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None +) -> torch.Tensor: + d, x = split_block_dims(blocks, 2) + d = d.view(torch.float16).to(dtype) + x = x.view(torch.int8) + return d * x + + +def dequantize_blocks_Q5_1( + blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None +) -> torch.Tensor: + n_blocks = blocks.shape[0] + + d, m, qh, qs = split_block_dims(blocks, 2, 2, 4) + d = d.view(torch.float16).to(dtype) + m = m.view(torch.float16).to(dtype) + qh = to_uint32(qh) + + qh = qh.reshape((n_blocks, 1)) >> torch.arange(32, device=d.device, dtype=torch.int32).reshape(1, 32) + ql = qs.reshape((n_blocks, -1, 1, block_size // 2)) >> torch.tensor( + [0, 4], device=d.device, dtype=torch.uint8 + ).reshape(1, 1, 2, 1) + qh = (qh & 1).to(torch.uint8) + ql = (ql & 0x0F).reshape((n_blocks, -1)) + + qs = ql | (qh << 4) + return (d * qs) + m + + +def dequantize_blocks_Q5_0( + blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None +) -> torch.Tensor: + n_blocks = blocks.shape[0] + + d, qh, qs = split_block_dims(blocks, 2, 4) + d = d.view(torch.float16).to(dtype) + qh = to_uint32(qh) + + qh = qh.reshape(n_blocks, 1) >> torch.arange(32, device=d.device, dtype=torch.int32).reshape(1, 32) + ql = qs.reshape(n_blocks, -1, 1, block_size // 2) >> torch.tensor( + [0, 4], device=d.device, dtype=torch.uint8 + ).reshape(1, 1, 2, 1) + + qh = (qh & 1).to(torch.uint8) + ql = (ql & 0x0F).reshape(n_blocks, -1) + + qs = (ql | (qh << 4)).to(torch.int8) - 16 + return d * qs + + +def dequantize_blocks_Q4_1( + blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None +) -> torch.Tensor: + n_blocks = blocks.shape[0] + + d, m, qs = split_block_dims(blocks, 2, 2) + d = d.view(torch.float16).to(dtype) + m = m.view(torch.float16).to(dtype) + + qs = qs.reshape((n_blocks, -1, 1, block_size // 2)) >> torch.tensor( + [0, 4], device=d.device, dtype=torch.uint8 + ).reshape(1, 1, 2, 1) + qs = (qs & 0x0F).reshape(n_blocks, -1) + + return (d * qs) + m + + +def dequantize_blocks_Q4_0( + blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None +) -> torch.Tensor: + n_blocks = blocks.shape[0] + + d, qs = split_block_dims(blocks, 2) + d = d.view(torch.float16).to(dtype) + + qs = qs.reshape((n_blocks, -1, 1, block_size // 2)) >> torch.tensor( + [0, 4], device=d.device, dtype=torch.uint8 + ).reshape((1, 1, 2, 1)) + qs = (qs & 0x0F).reshape((n_blocks, -1)).to(torch.int8) - 8 + return d * qs + + +def dequantize_blocks_BF16( + blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None +) -> torch.Tensor: + return (blocks.view(torch.int16).to(torch.int32) << 16).view(torch.float32) + + +def dequantize_blocks_Q6_K( + blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None +) -> torch.Tensor: + n_blocks = blocks.shape[0] + + ( + ql, + qh, + scales, + d, + ) = split_block_dims(blocks, QK_K // 2, QK_K // 4, QK_K // 16) + + scales = scales.view(torch.int8).to(dtype) + d = d.view(torch.float16).to(dtype) + d = (d * scales).reshape((n_blocks, QK_K // 16, 1)) + + ql = ql.reshape((n_blocks, -1, 1, 64)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape( + (1, 1, 2, 1) + ) + ql = (ql & 0x0F).reshape((n_blocks, -1, 32)) + qh = qh.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape( + (1, 1, 4, 1) + ) + qh = (qh & 0x03).reshape((n_blocks, -1, 32)) + q = (ql | (qh << 4)).to(torch.int8) - 32 + q = q.reshape((n_blocks, QK_K // 16, -1)) + + return (d * q).reshape((n_blocks, QK_K)) + + +def dequantize_blocks_Q5_K( + blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None +) -> torch.Tensor: + n_blocks = blocks.shape[0] + + d, dmin, scales, qh, qs = split_block_dims(blocks, 2, 2, K_SCALE_SIZE, QK_K // 8) + + d = d.view(torch.float16).to(dtype) + dmin = dmin.view(torch.float16).to(dtype) + + sc, m = get_scale_min(scales) + + d = (d * sc).reshape((n_blocks, -1, 1)) + dm = (dmin * m).reshape((n_blocks, -1, 1)) + + ql = qs.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape( + (1, 1, 2, 1) + ) + qh = qh.reshape((n_blocks, -1, 1, 32)) >> torch.tensor(list(range(8)), device=d.device, dtype=torch.uint8).reshape( + (1, 1, 8, 1) + ) + ql = (ql & 0x0F).reshape((n_blocks, -1, 32)) + qh = (qh & 0x01).reshape((n_blocks, -1, 32)) + q = ql | (qh << 4) + + return (d * q - dm).reshape((n_blocks, QK_K)) + + +def dequantize_blocks_Q4_K( + blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None +) -> torch.Tensor: + n_blocks = blocks.shape[0] + + d, dmin, scales, qs = split_block_dims(blocks, 2, 2, K_SCALE_SIZE) + d = d.view(torch.float16).to(dtype) + dmin = dmin.view(torch.float16).to(dtype) + + sc, m = get_scale_min(scales) + + d = (d * sc).reshape((n_blocks, -1, 1)) + dm = (dmin * m).reshape((n_blocks, -1, 1)) + + qs = qs.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape( + (1, 1, 2, 1) + ) + qs = (qs & 0x0F).reshape((n_blocks, -1, 32)) + + return (d * qs - dm).reshape((n_blocks, QK_K)) + + +def dequantize_blocks_Q3_K( + blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None +) -> torch.Tensor: + n_blocks = blocks.shape[0] + + hmask, qs, scales, d = split_block_dims(blocks, QK_K // 8, QK_K // 4, 12) + d = d.view(torch.float16).to(dtype) + + lscales, hscales = scales[:, :8], scales[:, 8:] + lscales = lscales.reshape((n_blocks, 1, 8)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape( + (1, 2, 1) + ) + lscales = lscales.reshape((n_blocks, 16)) + hscales = hscales.reshape((n_blocks, 1, 4)) >> torch.tensor( + [0, 2, 4, 6], device=d.device, dtype=torch.uint8 + ).reshape((1, 4, 1)) + hscales = hscales.reshape((n_blocks, 16)) + scales = (lscales & 0x0F) | ((hscales & 0x03) << 4) + scales = scales.to(torch.int8) - 32 + + dl = (d * scales).reshape((n_blocks, 16, 1)) + + ql = qs.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape( + (1, 1, 4, 1) + ) + qh = hmask.reshape(n_blocks, -1, 1, 32) >> torch.tensor(list(range(8)), device=d.device, dtype=torch.uint8).reshape( + (1, 1, 8, 1) + ) + ql = ql.reshape((n_blocks, 16, QK_K // 16)) & 3 + qh = (qh.reshape((n_blocks, 16, QK_K // 16)) & 1) ^ 1 + q = ql.to(torch.int8) - (qh << 2).to(torch.int8) + + return (dl * q).reshape((n_blocks, QK_K)) + + +def dequantize_blocks_Q2_K( + blocks: torch.Tensor, block_size: int, type_size: int, dtype: Optional[torch.dtype] = None +) -> torch.Tensor: + n_blocks = blocks.shape[0] + + scales, qs, d, dmin = split_block_dims(blocks, QK_K // 16, QK_K // 4, 2) + d = d.view(torch.float16).to(dtype) + dmin = dmin.view(torch.float16).to(dtype) + + # (n_blocks, 16, 1) + dl = (d * (scales & 0xF)).reshape((n_blocks, QK_K // 16, 1)) + ml = (dmin * (scales >> 4)).reshape((n_blocks, QK_K // 16, 1)) + + shift = torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape((1, 1, 4, 1)) + + qs = (qs.reshape((n_blocks, -1, 1, 32)) >> shift) & 3 + qs = qs.reshape((n_blocks, QK_K // 16, 16)) + qs = dl * qs - ml + + return qs.reshape((n_blocks, -1)) + + +DEQUANTIZE_FUNCTIONS: dict[ + gguf.GGMLQuantizationType, Callable[[torch.Tensor, int, int, Optional[torch.dtype]], torch.Tensor] +] = { + gguf.GGMLQuantizationType.BF16: dequantize_blocks_BF16, + gguf.GGMLQuantizationType.Q8_0: dequantize_blocks_Q8_0, + gguf.GGMLQuantizationType.Q5_1: dequantize_blocks_Q5_1, + gguf.GGMLQuantizationType.Q5_0: dequantize_blocks_Q5_0, + gguf.GGMLQuantizationType.Q4_1: dequantize_blocks_Q4_1, + gguf.GGMLQuantizationType.Q4_0: dequantize_blocks_Q4_0, + gguf.GGMLQuantizationType.Q6_K: dequantize_blocks_Q6_K, + gguf.GGMLQuantizationType.Q5_K: dequantize_blocks_Q5_K, + gguf.GGMLQuantizationType.Q4_K: dequantize_blocks_Q4_K, + gguf.GGMLQuantizationType.Q3_K: dequantize_blocks_Q3_K, + gguf.GGMLQuantizationType.Q2_K: dequantize_blocks_Q2_K, +} + + +def is_torch_compatible(tensor: Optional[torch.Tensor]): + return getattr(tensor, "tensor_type", None) in TORCH_COMPATIBLE_QTYPES + + +def is_quantized(tensor: torch.Tensor): + return not is_torch_compatible(tensor) + + +def dequantize( + data: torch.Tensor, qtype: gguf.GGMLQuantizationType, oshape: torch.Size, dtype: Optional[torch.dtype] = None +): + """ + Dequantize tensor back to usable shape/dtype + """ + block_size, type_size = gguf.GGML_QUANT_SIZES[qtype] + dequantize_blocks = DEQUANTIZE_FUNCTIONS[qtype] + + rows = data.reshape((-1, data.shape[-1])).view(torch.uint8) + + n_blocks = rows.numel() // type_size + blocks = rows.reshape((n_blocks, type_size)) + blocks = dequantize_blocks(blocks, block_size, type_size, dtype) + return blocks.reshape(oshape) + + +def to_uint32(x: torch.Tensor) -> torch.Tensor: + x = x.view(torch.uint8).to(torch.int32) + return (x[:, 0] | x[:, 1] << 8 | x[:, 2] << 16 | x[:, 3] << 24).unsqueeze(1) + + +def split_block_dims(blocks: torch.Tensor, *args): + n_max = blocks.shape[1] + dims = list(args) + [n_max - sum(args)] + return torch.split(blocks, dims, dim=1) + + +PATCH_TYPES = Union[torch.Tensor, list[torch.Tensor], tuple[torch.Tensor]] diff --git a/invokeai/backend/quantization/scripts/load_flux_model_bnb_llm_int8.py b/invokeai/backend/quantization/scripts/load_flux_model_bnb_llm_int8.py new file mode 100644 index 00000000000..8231e313fdc --- /dev/null +++ b/invokeai/backend/quantization/scripts/load_flux_model_bnb_llm_int8.py @@ -0,0 +1,80 @@ +from pathlib import Path + +import accelerate +from safetensors.torch import load_file, save_file + +from invokeai.backend.flux.model import Flux +from invokeai.backend.flux.util import get_flux_transformers_params +from invokeai.backend.model_manager.taxonomy import ModelVariantType +from invokeai.backend.quantization.bnb_llm_int8 import quantize_model_llm_int8 +from invokeai.backend.quantization.scripts.load_flux_model_bnb_nf4 import log_time + + +def main(): + """A script for quantizing a FLUX transformer model using the bitsandbytes LLM.int8() quantization method. + + This script is primarily intended for reference. The script params (e.g. the model_path, modules_to_not_convert, + etc.) are hardcoded and would need to be modified for other use cases. + """ + # Load the FLUX transformer model onto the meta device. + model_path = Path( + "/data/invokeai/models/.download_cache/https__huggingface.co_black-forest-labs_flux.1-schnell_resolve_main_flux1-schnell.safetensors/flux1-schnell.safetensors" + ) + + with log_time("Initialize FLUX transformer on meta device"): + # TODO(ryand): Determine if this is a schnell model or a dev model and load the appropriate config. + p = get_flux_transformers_params(ModelVariantType.FluxSchnell) + + # Initialize the model on the "meta" device. + with accelerate.init_empty_weights(): + model = Flux(p) + + # TODO(ryand): We may want to add some modules to not quantize here (e.g. the proj_out layer). See the accelerate + # `get_keys_to_not_convert(...)` function for a heuristic to determine which modules to not quantize. + modules_to_not_convert: set[str] = set() + + model_int8_path = model_path.parent / "bnb_llm_int8.safetensors" + if model_int8_path.exists(): + # The quantized model already exists, load it and return it. + print(f"A pre-quantized model already exists at '{model_int8_path}'. Attempting to load it...") + + # Replace the linear layers with LLM.int8() quantized linear layers (still on the meta device). + with log_time("Replace linear layers with LLM.int8() layers"), accelerate.init_empty_weights(): + model = quantize_model_llm_int8(model, modules_to_not_convert=modules_to_not_convert) + + with log_time("Load state dict into model"): + sd = load_file(model_int8_path) + model.load_state_dict(sd, strict=True, assign=True) + + with log_time("Move model to cuda"): + model = model.to("cuda") + + print(f"Successfully loaded pre-quantized model from '{model_int8_path}'.") + + else: + # The quantized model does not exist, quantize the model and save it. + print(f"No pre-quantized model found at '{model_int8_path}'. Quantizing the model...") + + with log_time("Replace linear layers with LLM.int8() layers"), accelerate.init_empty_weights(): + model = quantize_model_llm_int8(model, modules_to_not_convert=modules_to_not_convert) + + with log_time("Load state dict into model"): + state_dict = load_file(model_path) + # TODO(ryand): Cast the state_dict to the appropriate dtype? + model.load_state_dict(state_dict, strict=True, assign=True) + + with log_time("Move model to cuda and quantize"): + model = model.to("cuda") + + with log_time("Save quantized model"): + model_int8_path.parent.mkdir(parents=True, exist_ok=True) + save_file(model.state_dict(), model_int8_path) + + print(f"Successfully quantized and saved model to '{model_int8_path}'.") + + assert isinstance(model, Flux) + return model + + +if __name__ == "__main__": + main() diff --git a/invokeai/backend/quantization/scripts/load_flux_model_bnb_nf4.py b/invokeai/backend/quantization/scripts/load_flux_model_bnb_nf4.py new file mode 100644 index 00000000000..6a4ee3abf93 --- /dev/null +++ b/invokeai/backend/quantization/scripts/load_flux_model_bnb_nf4.py @@ -0,0 +1,97 @@ +import time +from contextlib import contextmanager +from pathlib import Path + +import accelerate +import torch +from safetensors.torch import load_file, save_file + +from invokeai.backend.flux.model import Flux +from invokeai.backend.flux.util import get_flux_transformers_params +from invokeai.backend.model_manager.taxonomy import ModelVariantType +from invokeai.backend.quantization.bnb_nf4 import quantize_model_nf4 + + +@contextmanager +def log_time(name: str): + """Helper context manager to log the time taken by a block of code.""" + start = time.time() + try: + yield None + finally: + end = time.time() + print(f"'{name}' took {end - start:.4f} secs") + + +def main(): + """A script for quantizing a FLUX transformer model using the bitsandbytes NF4 quantization method. + + This script is primarily intended for reference. The script params (e.g. the model_path, modules_to_not_convert, + etc.) are hardcoded and would need to be modified for other use cases. + """ + model_path = Path( + "/data/invokeai/models/.download_cache/https__huggingface.co_black-forest-labs_flux.1-schnell_resolve_main_flux1-schnell.safetensors/flux1-schnell.safetensors" + ) + + # inference_dtype = torch.bfloat16 + with log_time("Initialize FLUX transformer on meta device"): + # TODO(ryand): Determine if this is a schnell model or a dev model and load the appropriate config. + p = get_flux_transformers_params(ModelVariantType.FluxSchnell) + + # Initialize the model on the "meta" device. + with accelerate.init_empty_weights(): + model = Flux(p) + + # TODO(ryand): We may want to add some modules to not quantize here (e.g. the proj_out layer). See the accelerate + # `get_keys_to_not_convert(...)` function for a heuristic to determine which modules to not quantize. + modules_to_not_convert: set[str] = set() + + model_nf4_path = model_path.parent / "bnb_nf4.safetensors" + if model_nf4_path.exists(): + # The quantized model already exists, load it and return it. + print(f"A pre-quantized model already exists at '{model_nf4_path}'. Attempting to load it...") + + # Replace the linear layers with NF4 quantized linear layers (still on the meta device). + with log_time("Replace linear layers with NF4 layers"), accelerate.init_empty_weights(): + model = quantize_model_nf4( + model, modules_to_not_convert=modules_to_not_convert, compute_dtype=torch.bfloat16 + ) + + with log_time("Load state dict into model"): + state_dict = load_file(model_nf4_path) + model.load_state_dict(state_dict, strict=True, assign=True) + + with log_time("Move model to cuda"): + model = model.to("cuda") + + print(f"Successfully loaded pre-quantized model from '{model_nf4_path}'.") + + else: + # The quantized model does not exist, quantize the model and save it. + print(f"No pre-quantized model found at '{model_nf4_path}'. Quantizing the model...") + + with log_time("Replace linear layers with NF4 layers"), accelerate.init_empty_weights(): + model = quantize_model_nf4( + model, modules_to_not_convert=modules_to_not_convert, compute_dtype=torch.bfloat16 + ) + + with log_time("Load state dict into model"): + state_dict = load_file(model_path) + # TODO(ryand): Cast the state_dict to the appropriate dtype? + model.load_state_dict(state_dict, strict=True, assign=True) + + with log_time("Move model to cuda and quantize"): + model = model.to("cuda") + + with log_time("Save quantized model"): + model_nf4_path.parent.mkdir(parents=True, exist_ok=True) + save_file(model.state_dict(), model_nf4_path) + + print(f"Successfully quantized and saved model to '{model_nf4_path}'.") + + assert isinstance(model, Flux) + return model + + +if __name__ == "__main__": + main() diff --git a/invokeai/backend/quantization/scripts/quantize_t5_xxl_bnb_llm_int8.py b/invokeai/backend/quantization/scripts/quantize_t5_xxl_bnb_llm_int8.py new file mode 100644 index 00000000000..2e610404cdc --- /dev/null +++ b/invokeai/backend/quantization/scripts/quantize_t5_xxl_bnb_llm_int8.py @@ -0,0 +1,92 @@ +from pathlib import Path + +import accelerate +from safetensors.torch import load_file, save_file +from transformers import AutoConfig, AutoModelForTextEncoding, T5EncoderModel + +from invokeai.backend.quantization.bnb_llm_int8 import quantize_model_llm_int8 +from invokeai.backend.quantization.scripts.load_flux_model_bnb_nf4 import log_time + + +def load_state_dict_into_t5(model: T5EncoderModel, state_dict: dict): + # There is a shared reference to a single weight tensor in the model. + # Both "encoder.embed_tokens.weight" and "shared.weight" refer to the same tensor, so only the latter should + # be present in the state_dict. + missing_keys, unexpected_keys = model.load_state_dict(state_dict, strict=False, assign=True) + assert len(unexpected_keys) == 0 + assert set(missing_keys) == {"encoder.embed_tokens.weight"} + # Assert that the layers we expect to be shared are actually shared. + assert model.encoder.embed_tokens.weight is model.shared.weight + + +def main(): + """A script for quantizing a T5 text encoder model using the bitsandbytes LLM.int8() quantization method. + + This script is primarily intended for reference. The script params (e.g. the model_path, modules_to_not_convert, + etc.) are hardcoded and would need to be modified for other use cases. + """ + model_path = Path("/data/misc/text_encoder_2") + + with log_time("Initialize T5 on meta device"): + model_config = AutoConfig.from_pretrained(model_path) + with accelerate.init_empty_weights(): + model = AutoModelForTextEncoding.from_config(model_config) + + # TODO(ryand): We may want to add some modules to not quantize here (e.g. the proj_out layer). See the accelerate + # `get_keys_to_not_convert(...)` function for a heuristic to determine which modules to not quantize. + modules_to_not_convert: set[str] = set() + + model_int8_path = model_path / "bnb_llm_int8.safetensors" + if model_int8_path.exists(): + # The quantized model already exists, load it and return it. + print(f"A pre-quantized model already exists at '{model_int8_path}'. Attempting to load it...") + + # Replace the linear layers with LLM.int8() quantized linear layers (still on the meta device). + with log_time("Replace linear layers with LLM.int8() layers"), accelerate.init_empty_weights(): + model = quantize_model_llm_int8(model, modules_to_not_convert=modules_to_not_convert) + + with log_time("Load state dict into model"): + sd = load_file(model_int8_path) + load_state_dict_into_t5(model, sd) + + with log_time("Move model to cuda"): + model = model.to("cuda") + + print(f"Successfully loaded pre-quantized model from '{model_int8_path}'.") + + else: + # The quantized model does not exist, quantize the model and save it. + print(f"No pre-quantized model found at '{model_int8_path}'. Quantizing the model...") + + with log_time("Replace linear layers with LLM.int8() layers"), accelerate.init_empty_weights(): + model = quantize_model_llm_int8(model, modules_to_not_convert=modules_to_not_convert) + + with log_time("Load state dict into model"): + # Load sharded state dict. + files = list(model_path.glob("*.safetensors")) + state_dict = {} + for file in files: + sd = load_file(file) + state_dict.update(sd) + load_state_dict_into_t5(model, state_dict) + + with log_time("Move model to cuda and quantize"): + model = model.to("cuda") + + with log_time("Save quantized model"): + model_int8_path.parent.mkdir(parents=True, exist_ok=True) + state_dict = model.state_dict() + state_dict.pop("encoder.embed_tokens.weight") + save_file(state_dict, model_int8_path) + # This handling of shared weights could also be achieved with save_model(...), but then we'd lose control + # over which keys are kept. And, the corresponding load_model(...) function does not support assign=True. + # save_model(model, model_int8_path) + + print(f"Successfully quantized and saved model to '{model_int8_path}'.") + + assert isinstance(model, T5EncoderModel) + return model + + +if __name__ == "__main__": + main() diff --git a/invokeai/backend/raw_model.py b/invokeai/backend/raw_model.py new file mode 100644 index 00000000000..23502b20cb6 --- /dev/null +++ b/invokeai/backend/raw_model.py @@ -0,0 +1,22 @@ +from abc import ABC, abstractmethod +from typing import Optional + +import torch + + +class RawModel(ABC): + """Base class for 'Raw' models. + + The RawModel class is the base class of LoRAModelRaw, TextualInversionModelRaw, etc. + and is used for type checking of calls to the model patcher. Its main purpose + is to avoid a circular import issues when lora.py tries to import BaseModelType + from invokeai.backend.model_manager.config, and the latter tries to import LoRAModelRaw + from lora.py. + + The term 'raw' was introduced to describe a wrapper around a torch.nn.Module + that adds additional methods and attributes. + """ + + @abstractmethod + def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None: + pass diff --git a/invokeai/backend/rectified_flow/__init__.py b/invokeai/backend/rectified_flow/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/rectified_flow/er_sde_scheduler.py b/invokeai/backend/rectified_flow/er_sde_scheduler.py new file mode 100644 index 00000000000..7a30aa8cb6b --- /dev/null +++ b/invokeai/backend/rectified_flow/er_sde_scheduler.py @@ -0,0 +1,601 @@ +"""ER-SDE (Extended Reverse-time SDE) ``diffusers`` scheduler. + +Implements the multistep Taylor-expansion solver from: + + Cui, Q., Zhang, X., Lu, Z., & Liao, Q. (2023). + Elucidating the solution space of extended reverse-time SDE + for diffusion models. arXiv:2309.06169. + https://arxiv.org/abs/2309.06169 + +Reference implementation (MIT-licensed): + https://github.com/QinpengCui/ER-SDE-Solver/blob/main/er_sde_solver.py + +This scheduler unifies two regimes under a single API: + +* **VP-SDE** (``use_flow_sigmas=False``) — Stable Diffusion / SDXL style models + with epsilon, x0, or v prediction. Uses the standard + ``alpha_t = 1 / sqrt(1 + sigma^2), sigma_t = sigma * alpha_t`` parameterization + and ports ``vp_*_order_*`` from the reference impl. +* **Rectified flow / flow matching** (``use_flow_sigmas=True``) — FLUX, Z-Image, + Anima style models with flow_prediction. Uses ``alpha_t = 1 - sigma, sigma_t = sigma`` + and the rectified-flow integral helpers defined locally (``_fn``, + ``_integral_one_over_fn``, ``_integral_lam_minus_curr_over_fn``). + +The rectified-flow integral helpers are kept local so this class is self-contained. +""" + +from __future__ import annotations + +import math +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.schedulers.scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin, SchedulerOutput +from diffusers.utils.torch_utils import randn_tensor + +# Number of sample points for the left Riemann sums approximating the +# Taylor-extension integrals. Matches the reference impl's nums_intergrate=100. +_INTEGRAL_NUM_POINTS = 100 + + +def _fn(x: float) -> float: + """ER-SDE noise-scale function ``SDE_5`` (paper appendix A.8). + + Mirrors ``customized_func(..., func_type=7)`` in the reference impl — + the variant the paper recommends and tests for fast (~20 NFE) sampling. + """ + return x * (math.exp(x**0.3) + 10.0) + + +def _integral_one_over_fn(lambda_next: float, lambda_curr: float) -> float: + """Left Riemann sum of int_{lambda_next}^{lambda_curr} 1/_fn(lam) dlam. + + Precondition: ``lambda_next > 0``. The integrand has a logarithmic singularity + at ``lam = 0`` (``_fn(0) = 0``); callers must skip this when ``sigma_next == 0``. + """ + delta = lambda_curr - lambda_next + if delta <= 0: + return 0.0 + step = delta / _INTEGRAL_NUM_POINTS + total = 0.0 + for k in range(_INTEGRAL_NUM_POINTS): + lam = lambda_next + k * step + total += step / _fn(lam) + return total + + +def _integral_lam_minus_curr_over_fn(lambda_next: float, lambda_curr: float) -> float: + """Left Riemann sum of int_{lambda_next}^{lambda_curr} (lam - lambda_curr)/_fn(lam) dlam. + + Precondition: ``lambda_next > 0``. Same singularity at ``lam = 0`` as + :func:`_integral_one_over_fn`. + """ + delta = lambda_curr - lambda_next + if delta <= 0: + return 0.0 + step = delta / _INTEGRAL_NUM_POINTS + total = 0.0 + for k in range(_INTEGRAL_NUM_POINTS): + lam = lambda_next + k * step + total += step * (lam - lambda_curr) / _fn(lam) + return total + + +class ERSDEScheduler(SchedulerMixin, ConfigMixin): + """``diffusers`` scheduler for the ER-SDE multistep solver. + + See module docstring for paper / reference-impl citations. + + Args: + num_train_timesteps: Number of diffusion steps used during training. + beta_start: VP-SDE beta schedule start (ignored when ``use_flow_sigmas=True``). + beta_end: VP-SDE beta schedule end (ignored when ``use_flow_sigmas=True``). + beta_schedule: ``"linear"``, ``"scaled_linear"``, or ``"squaredcos_cap_v2"``. + trained_betas: Override betas with a pre-computed schedule. + prediction_type: ``"epsilon"``, ``"v_prediction"``, or ``"flow_prediction"``. + solver_order: Multistep order (1, 2, or 3). The solver auto-warms from order 1. + use_flow_sigmas: If True, use the rectified-flow parameterization + (``alpha_t = 1 - sigma``); else VP-SDE. + flow_shift: Sigma shift applied to the default flow schedule. + stochastic: If True, inject noise (full ER-SDE). If False, deterministic + ODE companion — same Taylor expansion with the noise term zeroed. + sigma_one_tolerance: Boundary tolerance for the ``sigma = 1`` limit + (rectified-flow only). Numerically paranoid; keep small. + timestep_spacing: ``"linspace"``, ``"leading"``, or ``"trailing"``. + steps_offset: Offset added to ``"leading"`` timesteps. + """ + + _compatibles = [e.name for e in KarrasDiffusionSchedulers] + order = 1 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.00085, + beta_end: float = 0.012, + beta_schedule: str = "scaled_linear", + trained_betas: Optional[Union[np.ndarray, List[float]]] = None, + prediction_type: str = "epsilon", + solver_order: int = 3, + use_flow_sigmas: bool = False, + flow_shift: float = 1.0, + stochastic: bool = True, + sigma_one_tolerance: float = 1e-6, + timestep_spacing: str = "linspace", + steps_offset: int = 0, + ): + if prediction_type not in ("epsilon", "v_prediction", "flow_prediction"): + raise ValueError( + f"prediction_type must be one of 'epsilon', 'v_prediction', 'flow_prediction', got {prediction_type!r}" + ) + if solver_order not in (1, 2, 3): + raise ValueError(f"solver_order must be 1, 2, or 3, got {solver_order}") + if prediction_type == "flow_prediction" and not use_flow_sigmas: + # Not strictly invalid, but almost certainly a misconfiguration. + raise ValueError("prediction_type='flow_prediction' requires use_flow_sigmas=True (rectified-flow regime).") + + # VP-SDE noise schedule (only used when use_flow_sigmas=False). + if trained_betas is not None: + self.betas = torch.tensor(trained_betas, dtype=torch.float32) + elif beta_schedule == "linear": + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32) + elif beta_schedule == "scaled_linear": + self.betas = torch.linspace(beta_start**0.5, beta_end**0.5, num_train_timesteps, dtype=torch.float32) ** 2 + elif beta_schedule == "squaredcos_cap_v2": + # Glide cosine schedule. + betas = [] + for i in range(num_train_timesteps): + t1 = i / num_train_timesteps + t2 = (i + 1) / num_train_timesteps + a1 = math.cos((t1 + 0.008) / 1.008 * math.pi / 2) ** 2 + a2 = math.cos((t2 + 0.008) / 1.008 * math.pi / 2) ** 2 + betas.append(min(1 - a2 / a1, 0.999)) + self.betas = torch.tensor(betas, dtype=torch.float32) + else: + raise NotImplementedError(f"beta_schedule {beta_schedule!r} is not implemented for ERSDEScheduler") + + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + + # Default sigmas (VP-SDE form). Overwritten in set_timesteps. + self.sigmas = ((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5 + + # Standard deviation of initial noise distribution (per Euler convention). + self.init_noise_sigma = 1.0 + + self.num_inference_steps: Optional[int] = None + timesteps = np.linspace(0, num_train_timesteps - 1, num_train_timesteps, dtype=np.float32)[::-1].copy() + self.timesteps = torch.from_numpy(timesteps) + + # Multistep history. ``model_outputs`` stores x0 predictions; ``_sigma_history`` + # stores the sigma at which each prediction was made. Both are FIFO with + # length == solver_order. Slot ``-1`` is the most recent. + self.model_outputs: List[Optional[torch.Tensor]] = [None] * solver_order + self._sigma_history: List[Optional[float]] = [None] * solver_order + self.lower_order_nums = 0 + self._step_index: Optional[int] = None + self._begin_index: Optional[int] = None + self.sigmas = self.sigmas.to("cpu") + + # ---- Index plumbing (mirrors DPM++) --------------------------------------- + + @property + def step_index(self) -> Optional[int]: + return self._step_index + + @property + def begin_index(self) -> Optional[int]: + return self._begin_index + + def set_begin_index(self, begin_index: int = 0) -> None: + self._begin_index = begin_index + + def index_for_timestep( + self, + timestep: Union[int, torch.Tensor], + schedule_timesteps: Optional[torch.Tensor] = None, + ) -> int: + if schedule_timesteps is None: + schedule_timesteps = self.timesteps + index_candidates = (schedule_timesteps == timestep).nonzero() + if len(index_candidates) == 0: + return len(self.timesteps) - 1 + # On the very first step, prefer the second match if duplicated, so + # img2img doesn't accidentally skip a sigma. + if len(index_candidates) > 1: + return index_candidates[1].item() + return index_candidates[0].item() + + def _init_step_index(self, timestep: Union[int, torch.Tensor]) -> None: + if self.begin_index is None: + if isinstance(timestep, torch.Tensor): + timestep = timestep.to(self.timesteps.device) + self._step_index = self.index_for_timestep(timestep) + else: + self._step_index = self._begin_index + + # ---- Timestep / sigma scheduling ------------------------------------------ + + def set_timesteps( + self, + num_inference_steps: Optional[int] = None, + device: Optional[Union[str, torch.device]] = None, + sigmas: Optional[Union[List[float], np.ndarray, torch.Tensor]] = None, + timesteps: Optional[List[int]] = None, + ) -> None: + """Set the discrete timesteps used for inference. + + Exactly one of ``num_inference_steps``, ``timesteps``, or ``sigmas`` must + be provided. The ``sigmas`` form (mirroring :class:`EulerDiscreteScheduler`) + lets Anima/FLUX/Z-Image inject pre-shifted sigma schedules directly. + """ + n_set = sum(x is not None for x in (num_inference_steps, timesteps, sigmas)) + if n_set != 1: + raise ValueError("Must pass exactly one of `num_inference_steps`, `timesteps`, or `sigmas`.") + + if sigmas is not None: + if isinstance(sigmas, torch.Tensor): + sigmas_np = sigmas.detach().cpu().numpy().astype(np.float32) + else: + sigmas_np = np.array(sigmas, dtype=np.float32) + num_inference_steps = len(sigmas_np) - 1 + # Timesteps in the rectified-flow / Anima convention scale sigma to t. + # For VP-SDE this approximation is wrong but timesteps are only used + # for indexing; the algebra runs entirely off self.sigmas. + timesteps_np = (sigmas_np[:-1] * self.config.num_train_timesteps).astype(np.float32) + elif timesteps is not None: + timesteps_np = np.array(timesteps, dtype=np.float32) + num_inference_steps = len(timesteps_np) + sigmas_np = self._sigmas_for_timesteps(timesteps_np) + else: + assert num_inference_steps is not None + timesteps_np = self._default_timesteps(num_inference_steps) + sigmas_np = self._sigmas_for_timesteps(timesteps_np) + + self.num_inference_steps = num_inference_steps + self.sigmas = torch.from_numpy(sigmas_np.astype(np.float32)) + self.timesteps = torch.from_numpy(timesteps_np.astype(np.float32)).to(device=device) + + # Reset multistep state. + self.model_outputs = [None] * self.config.solver_order + self._sigma_history = [None] * self.config.solver_order + self.lower_order_nums = 0 + self._step_index = None + self._begin_index = None + self.sigmas = self.sigmas.to("cpu") + + def _default_timesteps(self, num_inference_steps: int) -> np.ndarray: + """Standard linspace/leading/trailing schedule (VP-SDE timesteps).""" + if self.config.timestep_spacing == "linspace": + timesteps = ( + np.linspace(0, self.config.num_train_timesteps - 1, num_inference_steps + 1) + .round()[::-1][:-1] + .copy() + .astype(np.float32) + ) + elif self.config.timestep_spacing == "leading": + step_ratio = self.config.num_train_timesteps // (num_inference_steps + 1) + timesteps = ( + (np.arange(0, num_inference_steps + 1) * step_ratio).round()[::-1][:-1].copy().astype(np.float32) + ) + timesteps += self.config.steps_offset + elif self.config.timestep_spacing == "trailing": + step_ratio = self.config.num_train_timesteps / num_inference_steps + timesteps = np.arange(self.config.num_train_timesteps, 0, -step_ratio).round().copy().astype(np.float32) + timesteps -= 1 + else: + raise ValueError( + f"timestep_spacing {self.config.timestep_spacing!r} must be one of 'linspace', 'leading', 'trailing'" + ) + return timesteps + + def _sigmas_for_timesteps(self, timesteps_np: np.ndarray) -> np.ndarray: + """Build the sigma schedule (with terminal 0 appended) for given timesteps.""" + if self.config.use_flow_sigmas: + # Rectified-flow sigmas in [0, 1], time-shifted per Anima/FLUX convention. + num_inference_steps = len(timesteps_np) + alphas = np.linspace(1, 1 / self.config.num_train_timesteps, num_inference_steps + 1) + sigmas = 1.0 - alphas + shift = self.config.flow_shift + sigmas = np.flip(shift * sigmas / (1 + (shift - 1) * sigmas))[:-1].copy() + # Terminal sigma is exactly 0. + return np.concatenate([sigmas, [0.0]]).astype(np.float32) + + # VP-SDE: interpolate against the train sigmas using timestep indexing. + train_sigmas = np.array(((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5) + sigmas = np.interp(timesteps_np, np.arange(0, len(train_sigmas)), train_sigmas) + return np.concatenate([sigmas, [0.0]]).astype(np.float32) + + # ---- Math helpers --------------------------------------------------------- + + def _sigma_to_alpha_sigma_t(self, sigma: float) -> Tuple[float, float]: + """Map ``sigma`` to ``(alpha_t, sigma_t)``. + + Rectified flow: ``alpha_t = 1 - sigma, sigma_t = sigma``. + VP-SDE: ``alpha_t = 1 / sqrt(1 + sigma^2), sigma_t = sigma * alpha_t``. + """ + if self.config.use_flow_sigmas: + return 1.0 - sigma, sigma + alpha_t = 1.0 / math.sqrt(1.0 + sigma * sigma) + return alpha_t, sigma * alpha_t + + @staticmethod + def _lambda(alpha_t: float, sigma_t: float) -> float: + """ER-SDE ``lambda = sigma_t / alpha_t`` — the noise-to-signal ratio. + + This matches the reference impl's ``lambdas = sigmas / alphas`` in both + VP and rectified-flow regimes (see ``vp_*_order_*`` in + ``https://github.com/QinpengCui/ER-SDE-Solver``). For VP-SDE this equals + the stored sigma; for rectified flow it equals ``sigma / (1 - sigma)``. + Diverges at ``sigma_t = alpha_t = 0`` (rectified flow at sigma=1) — the + boundary branch in :meth:`_first_order_update` handles that case. + """ + if alpha_t == 0.0: + return float("inf") + return sigma_t / alpha_t + + # ---- Model output conversion ---------------------------------------------- + + def _convert_model_output(self, model_output: torch.Tensor, sample: torch.Tensor) -> torch.Tensor: + """Convert raw model output to an ``x0`` prediction at the current sigma.""" + sigma = float(self.sigmas[self.step_index].item()) + if self.config.prediction_type == "flow_prediction": + # v = (x - x0) / sigma => x0 = x - sigma * v + return sample - sigma * model_output + alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma) + if self.config.prediction_type == "epsilon": + return (sample - sigma_t * model_output) / alpha_t + if self.config.prediction_type == "v_prediction": + return alpha_t * sample - sigma_t * model_output + raise ValueError(f"Unsupported prediction_type {self.config.prediction_type!r}") + + # ---- Order-N updates ------------------------------------------------------- + + def _first_order_update( + self, + x0: torch.Tensor, + sample: torch.Tensor, + sigma_curr: float, + sigma_next: float, + noise: Optional[torch.Tensor], + ) -> torch.Tensor: + """Order-1 ER-SDE step (ports ``vp_1_order`` / ``er_sde_rf_step`` order-1 branch).""" + # Rectified-flow boundary: sigma_curr ~= 1 means alpha_curr ~= 0 so lambda diverges. + # Closed-form limit (er_sde.py:136-142): x_next = (1 - sigma_next) * x0 + sigma_next * noise. + if self.config.use_flow_sigmas and 1.0 - sigma_curr < self.config.sigma_one_tolerance: + x_next = (1.0 - sigma_next) * x0 + if self.config.stochastic and noise is not None and sigma_next > 0.0: + x_next = x_next + sigma_next * noise + return x_next + + alpha_curr, sigma_curr_t = self._sigma_to_alpha_sigma_t(sigma_curr) + alpha_next, sigma_next_t = self._sigma_to_alpha_sigma_t(sigma_next) + + # Reference impl uses lambda = sigma_t / alpha_t in both VP and flow regimes. + lambda_curr = self._lambda(alpha_curr, sigma_curr_t) + # At the terminal step, sigma_next == 0 so lambda_next == 0 and fn_next == 0. + lambda_next = self._lambda(alpha_next, sigma_next_t) if sigma_next_t > 0.0 else 0.0 + + fn_curr = _fn(lambda_curr) + fn_next = _fn(lambda_next) + r_fn = fn_next / fn_curr if fn_curr != 0.0 else 0.0 + r_alphas = alpha_next / alpha_curr + + # Stochastic noise std (paper appendix eq. for ER-SDE_5 variance). + # ``inner`` can underflow to tiny negatives by roundoff; clip. + inner = lambda_next**2 - lambda_curr**2 * r_fn**2 + if inner < 0.0: + inner = 0.0 + noise_std = math.sqrt(inner) * alpha_next + + x_next = r_alphas * r_fn * sample + alpha_next * (1.0 - r_fn) * x0 + if self.config.stochastic and noise is not None and sigma_next > 0.0: + x_next = x_next + noise_std * noise + return x_next + + def _second_order_update( + self, + sample: torch.Tensor, + sigma_curr: float, + sigma_next: float, + noise: Optional[torch.Tensor], + ) -> torch.Tensor: + """Order-2 ER-SDE step (ports ``vp_2_order_taylor``).""" + x0 = self.model_outputs[-1] + old_x0 = self.model_outputs[-2] + sigma_prev_curr = self._sigma_history[-2] + assert x0 is not None and old_x0 is not None and sigma_prev_curr is not None + + # If the previous step used the sigma=1 closed-form limit, the finite-difference + # derivative across that boundary is meaningless — fall back to order 1. + if self.config.use_flow_sigmas and 1.0 - sigma_prev_curr < self.config.sigma_one_tolerance: + return self._first_order_update(x0, sample, sigma_curr, sigma_next, noise) + + # Order-1 base. + x_next = self._first_order_update(x0, sample, sigma_curr, sigma_next, noise) + + # Skip the higher-order term at the terminal step — the integral helpers diverge + # at lambda = 0 (sigma = 0), see _integral_one_over_fn docstring. + if sigma_next <= 0.0: + return x_next + + alpha_curr, sigma_curr_t = self._sigma_to_alpha_sigma_t(sigma_curr) + alpha_next, sigma_next_t = self._sigma_to_alpha_sigma_t(sigma_next) + alpha_prev, sigma_prev_t = self._sigma_to_alpha_sigma_t(sigma_prev_curr) + lambda_curr = self._lambda(alpha_curr, sigma_curr_t) + lambda_next = self._lambda(alpha_next, sigma_next_t) + lambda_prev = self._lambda(alpha_prev, sigma_prev_t) + + denom = lambda_curr - lambda_prev + if denom == 0.0: + return x_next + d_x0 = (x0 - old_x0) / denom + + fn_next = _fn(lambda_next) + s_int = _integral_one_over_fn(lambda_next, lambda_curr) + x_next = x_next + alpha_next * (lambda_next - lambda_curr + s_int * fn_next) * d_x0 + return x_next + + def _third_order_update( + self, + sample: torch.Tensor, + sigma_curr: float, + sigma_next: float, + noise: Optional[torch.Tensor], + ) -> torch.Tensor: + """Order-3 ER-SDE step (ports ``vp_3_order_taylor``).""" + x0 = self.model_outputs[-1] + old_x0 = self.model_outputs[-2] + old_old_x0 = self.model_outputs[-3] + sigma_prev_curr = self._sigma_history[-2] + sigma_prev_prev = self._sigma_history[-3] + assert ( + x0 is not None + and old_x0 is not None + and old_old_x0 is not None + and sigma_prev_curr is not None + and sigma_prev_prev is not None + ) + + # If any sigma in the lookback hits the boundary, fall back to order 2. + if self.config.use_flow_sigmas and ( + 1.0 - sigma_prev_curr < self.config.sigma_one_tolerance + or 1.0 - sigma_prev_prev < self.config.sigma_one_tolerance + ): + return self._second_order_update(sample, sigma_curr, sigma_next, noise) + + # Order-2 base. + x_next = self._second_order_update(sample, sigma_curr, sigma_next, noise) + + if sigma_next <= 0.0: + return x_next + + alpha_curr, sigma_curr_t = self._sigma_to_alpha_sigma_t(sigma_curr) + alpha_next, sigma_next_t = self._sigma_to_alpha_sigma_t(sigma_next) + alpha_prev, sigma_prev_t = self._sigma_to_alpha_sigma_t(sigma_prev_curr) + alpha_pprev, sigma_pprev_t = self._sigma_to_alpha_sigma_t(sigma_prev_prev) + lambda_curr = self._lambda(alpha_curr, sigma_curr_t) + lambda_next = self._lambda(alpha_next, sigma_next_t) + lambda_prev = self._lambda(alpha_prev, sigma_prev_t) + lambda_pprev = self._lambda(alpha_pprev, sigma_pprev_t) + + denom_d = lambda_curr - lambda_prev + denom_d_prev = lambda_prev - lambda_pprev + denom_dd = lambda_curr - lambda_pprev + if denom_d == 0.0 or denom_d_prev == 0.0 or denom_dd == 0.0: + return x_next + + d_x0 = (x0 - old_x0) / denom_d + old_d_x0 = (old_x0 - old_old_x0) / denom_d_prev + dd_x0 = 2.0 * (d_x0 - old_d_x0) / denom_dd + + fn_next = _fn(lambda_next) + s_d_int = _integral_lam_minus_curr_over_fn(lambda_next, lambda_curr) + x_next = x_next + alpha_next * ((lambda_next - lambda_curr) ** 2 / 2.0 + s_d_int * fn_next) * dd_x0 + return x_next + + # ---- Public step ---------------------------------------------------------- + + def scale_model_input( + self, sample: torch.Tensor, timestep: Optional[Union[int, torch.Tensor]] = None + ) -> torch.Tensor: + """No-op (matches ``FlowMatchEulerDiscreteScheduler``).""" + return sample + + def step( + self, + model_output: torch.Tensor, + timestep: Union[int, torch.Tensor], + sample: torch.Tensor, + generator: Optional[torch.Generator] = None, + return_dict: bool = True, + ) -> Union[SchedulerOutput, Tuple]: + """Predict the sample at the next timestep using one ER-SDE step.""" + if self.num_inference_steps is None: + raise ValueError("num_inference_steps is None — call `set_timesteps` before calling `step`.") + if self.step_index is None: + self._init_step_index(timestep) + + sigma_curr = float(self.sigmas[self.step_index].item()) + sigma_next = float(self.sigmas[self.step_index + 1].item()) + + # 1. Convert model output to x0 prediction. + x0 = self._convert_model_output(model_output, sample) + + # 2. FIFO-shift the multistep history. New entry goes in slot -1. + for i in range(self.config.solver_order - 1): + self.model_outputs[i] = self.model_outputs[i + 1] + self._sigma_history[i] = self._sigma_history[i + 1] + self.model_outputs[-1] = x0 + self._sigma_history[-1] = sigma_curr + + # 3. Sample noise (only when stochastic and not at terminal step). + if self.config.stochastic and sigma_next > 0.0: + noise = randn_tensor( + model_output.shape, + generator=generator, + device=model_output.device, + dtype=model_output.dtype, + ) + else: + noise = None + + # 4. Dispatch by available history. + if self.config.solver_order == 1 or self.lower_order_nums < 1: + prev_sample = self._first_order_update(x0, sample, sigma_curr, sigma_next, noise) + elif self.config.solver_order == 2 or self.lower_order_nums < 2: + prev_sample = self._second_order_update(sample, sigma_curr, sigma_next, noise) + else: + prev_sample = self._third_order_update(sample, sigma_curr, sigma_next, noise) + + if self.lower_order_nums < self.config.solver_order: + self.lower_order_nums += 1 + + # 5. Advance step index. + self._step_index += 1 + + if not return_dict: + return (prev_sample,) + return SchedulerOutput(prev_sample=prev_sample) + + # ---- Forward noising (training / img2img) --------------------------------- + + def add_noise( + self, + original_samples: torch.Tensor, + noise: torch.Tensor, + timesteps: torch.Tensor, + ) -> torch.Tensor: + """Forward-noise ``original_samples`` at the given timesteps (img2img style).""" + sigmas = self.sigmas.to(device=original_samples.device, dtype=original_samples.dtype) + if original_samples.device.type == "mps" and torch.is_floating_point(timesteps): + schedule_timesteps = self.timesteps.to(original_samples.device, dtype=torch.float32) + timesteps = timesteps.to(original_samples.device, dtype=torch.float32) + else: + schedule_timesteps = self.timesteps.to(original_samples.device) + timesteps = timesteps.to(original_samples.device) + + if self.begin_index is None: + step_indices = [self.index_for_timestep(t, schedule_timesteps) for t in timesteps] + elif self.step_index is not None: + step_indices = [self.step_index] * timesteps.shape[0] + else: + step_indices = [self.begin_index] * timesteps.shape[0] + + sigma = sigmas[step_indices].flatten() + while len(sigma.shape) < len(original_samples.shape): + sigma = sigma.unsqueeze(-1) + + if self.config.use_flow_sigmas: + alpha_t = 1.0 - sigma + sigma_t = sigma + else: + alpha_t = 1.0 / torch.sqrt(1.0 + sigma * sigma) + sigma_t = sigma * alpha_t + return alpha_t * original_samples + sigma_t * noise + + def __len__(self) -> int: + return self.config.num_train_timesteps diff --git a/invokeai/backend/rectified_flow/rectified_flow_inpaint_extension.py b/invokeai/backend/rectified_flow/rectified_flow_inpaint_extension.py new file mode 100644 index 00000000000..16a7ff8c69d --- /dev/null +++ b/invokeai/backend/rectified_flow/rectified_flow_inpaint_extension.py @@ -0,0 +1,58 @@ +import torch + + +def assert_broadcastable(*shapes): + try: + torch.broadcast_shapes(*shapes) + except RuntimeError as e: + raise AssertionError(f"Shapes {shapes} are not broadcastable.") from e + + +class RectifiedFlowInpaintExtension: + """A class for managing inpainting with rectified flow models (e.g. FLUX, SD3, CogView4).""" + + def __init__(self, init_latents: torch.Tensor, inpaint_mask: torch.Tensor, noise: torch.Tensor): + """Initialize InpaintExtension. + + Args: + init_latents (torch.Tensor): The initial latents (i.e. un-noised at timestep 0). In 'packed' format. + inpaint_mask (torch.Tensor): A mask specifying which elements to inpaint. Range [0, 1]. Values of 1 will be + re-generated. Values of 0 will remain unchanged. Values between 0 and 1 can be used to blend the + inpainted region with the background. In 'packed' format. + noise (torch.Tensor): The noise tensor used to noise the init_latents. In 'packed' format. + """ + assert_broadcastable(init_latents.shape, inpaint_mask.shape, noise.shape) + + self._init_latents = init_latents + self._inpaint_mask = inpaint_mask + self._noise = noise + + def _apply_mask_gradient_adjustment(self, t_prev: float) -> torch.Tensor: + """Applies inpaint mask gradient adjustment and returns the inpaint mask to be used at the current timestep.""" + # As we progress through the denoising process, we promote gradient regions of the mask to have a full weight of + # 1.0. This helps to produce more coherent seams around the inpainted region. + + # We use a small epsilon to avoid any potential issues with floating point precision. + eps = 1e-4 + mask = torch.where(self._inpaint_mask >= t_prev + eps, 1.0, 0.0).to( + dtype=self._inpaint_mask.dtype, device=self._inpaint_mask.device + ) + + return mask + + def merge_intermediate_latents_with_init_latents( + self, intermediate_latents: torch.Tensor, t_prev: float + ) -> torch.Tensor: + """Merge the intermediate latents with the initial latents for the current timestep using the inpaint mask. I.e. + update the intermediate latents to keep the regions that are not being inpainted on the correct noise + trajectory. + + This function should be called after each denoising step. + """ + mask = self._apply_mask_gradient_adjustment(t_prev) + + # Noise the init latents for the current timestep. + noised_init_latents = self._noise * t_prev + (1.0 - t_prev) * self._init_latents + + # Merge the intermediate latents with the noised_init_latents using the inpaint_mask. + return intermediate_latents * mask + noised_init_latents * (1.0 - mask) diff --git a/invokeai/backend/sig_lip/sig_lip_pipeline.py b/invokeai/backend/sig_lip/sig_lip_pipeline.py new file mode 100644 index 00000000000..db5cff5e2c8 --- /dev/null +++ b/invokeai/backend/sig_lip/sig_lip_pipeline.py @@ -0,0 +1,20 @@ +import torch +from PIL import Image +from transformers import SiglipImageProcessor, SiglipVisionModel + + +class SigLipPipeline: + """A wrapper for a SigLIP model + processor.""" + + def __init__( + self, + siglip_processor: SiglipImageProcessor, + siglip_model: SiglipVisionModel, + ): + self._siglip_processor = siglip_processor + self._siglip_model = siglip_model + + def encode_image(self, x: Image.Image, device: torch.device, dtype: torch.dtype) -> torch.Tensor: + imgs = self._siglip_processor.preprocess(images=[x], do_resize=True, return_tensors="pt", do_convert_rgb=True) + encoded_x = self._siglip_model(**imgs.to(device=device, dtype=dtype)).last_hidden_state + return encoded_x diff --git a/invokeai/backend/spandrel_image_to_image_model.py b/invokeai/backend/spandrel_image_to_image_model.py new file mode 100644 index 00000000000..ccf02c57ac0 --- /dev/null +++ b/invokeai/backend/spandrel_image_to_image_model.py @@ -0,0 +1,139 @@ +from pathlib import Path +from typing import Any, Optional + +import numpy as np +import torch +from PIL import Image +from spandrel import ImageModelDescriptor, ModelLoader + +from invokeai.backend.raw_model import RawModel + + +class SpandrelImageToImageModel(RawModel): + """A wrapper for a Spandrel Image-to-Image model. + + The main reason for having a wrapper class is to integrate with the type handling of RawModel. + """ + + def __init__(self, spandrel_model: ImageModelDescriptor[Any]): + self._spandrel_model = spandrel_model + + @staticmethod + def pil_to_tensor(image: Image.Image) -> torch.Tensor: + """Convert PIL Image to the torch.Tensor format expected by SpandrelImageToImageModel.run(). + + Args: + image (Image.Image): A PIL Image with shape (H, W, C) and values in the range [0, 255]. + + Returns: + torch.Tensor: A torch.Tensor with shape (N, C, H, W) and values in the range [0, 1]. + """ + image_np = np.array(image) + # (H, W, C) -> (C, H, W) + image_np = np.transpose(image_np, (2, 0, 1)) + image_np = image_np / 255 + image_tensor = torch.from_numpy(image_np).float() + # (C, H, W) -> (N, C, H, W) + image_tensor = image_tensor.unsqueeze(0) + return image_tensor + + @staticmethod + def tensor_to_pil(tensor: torch.Tensor) -> Image.Image: + """Convert a torch.Tensor produced by SpandrelImageToImageModel.run() to a PIL Image. + + Args: + tensor (torch.Tensor): A torch.Tensor with shape (N, C, H, W) and values in the range [0, 1]. + + Returns: + Image.Image: A PIL Image with shape (H, W, C) and values in the range [0, 255]. + """ + # (N, C, H, W) -> (C, H, W) + tensor = tensor.squeeze(0) + # (C, H, W) -> (H, W, C) + tensor = tensor.permute(1, 2, 0) + tensor = tensor.clamp(0, 1) + tensor = (tensor * 255).cpu().detach().numpy().astype(np.uint8) + image = Image.fromarray(tensor) + return image + + def run(self, image_tensor: torch.Tensor) -> torch.Tensor: + """Run the image-to-image model. + + Args: + image_tensor (torch.Tensor): A torch.Tensor with shape (N, C, H, W) and values in the range [0, 1]. + """ + return self._spandrel_model(image_tensor) + + @classmethod + def load_from_file(cls, file_path: str | Path): + model = ModelLoader().load_from_file(file_path) + if not isinstance(model, ImageModelDescriptor): + raise ValueError( + f"Loaded a spandrel model of type '{type(model)}'. Only image-to-image models are supported " + "('ImageModelDescriptor')." + ) + + return cls(spandrel_model=model) + + @classmethod + def load_from_state_dict(cls, state_dict: dict[str, torch.Tensor]): + model = ModelLoader().load_from_state_dict(state_dict) + if not isinstance(model, ImageModelDescriptor): + raise ValueError( + f"Loaded a spandrel model of type '{type(model)}'. Only image-to-image models are supported " + "('ImageModelDescriptor')." + ) + + return cls(spandrel_model=model) + + def supports_dtype(self, dtype: torch.dtype) -> bool: + """Check if the model supports the given dtype.""" + if dtype == torch.float16: + return self._spandrel_model.supports_half + elif dtype == torch.bfloat16: + return self._spandrel_model.supports_bfloat16 + elif dtype == torch.float32: + # All models support float32. + return True + else: + raise ValueError(f"Unexpected dtype '{dtype}'.") + + def get_model_type_name(self) -> str: + """The model type name. Intended for logging / debugging purposes. Do not rely on this field remaining + consistent over time. + """ + return str(type(self._spandrel_model.model)) + + def to( + self, + device: Optional[torch.device] = None, + dtype: Optional[torch.dtype] = None, + non_blocking: bool = False, + ) -> None: + """Note: Some models have limited dtype support. Call supports_dtype(...) to check if the dtype is supported. + Note: The non_blocking parameter is currently ignored.""" + # TODO(ryand): spandrel.ImageModelDescriptor.to(...) does not support non_blocking. We will have to access the + # model directly if we want to apply this optimization. + self._spandrel_model.to(device=device, dtype=dtype) + + @property + def device(self) -> torch.device: + """The device of the underlying model.""" + return self._spandrel_model.device + + @property + def dtype(self) -> torch.dtype: + """The dtype of the underlying model.""" + return self._spandrel_model.dtype + + @property + def scale(self) -> int: + """The scale of the model (e.g. 1x, 2x, 4x, etc.).""" + return self._spandrel_model.scale + + def calc_size(self) -> int: + """Get size of the model in memory in bytes.""" + # HACK(ryand): Fix this issue with circular imports. + from invokeai.backend.model_manager.load.model_util import calc_module_size + + return calc_module_size(self._spandrel_model.model) diff --git a/invokeai/backend/stable_diffusion/__init__.py b/invokeai/backend/stable_diffusion/__init__.py new file mode 100644 index 00000000000..6a6f2ebc49c --- /dev/null +++ b/invokeai/backend/stable_diffusion/__init__.py @@ -0,0 +1,15 @@ +""" +Initialization file for the invokeai.backend.stable_diffusion package +""" + +from invokeai.backend.stable_diffusion.diffusers_pipeline import ( # noqa: F401 + PipelineIntermediateState, + StableDiffusionGeneratorPipeline, +) +from invokeai.backend.stable_diffusion.diffusion import InvokeAIDiffuserComponent # noqa: F401 + +__all__ = [ + "PipelineIntermediateState", + "StableDiffusionGeneratorPipeline", + "InvokeAIDiffuserComponent", +] diff --git a/invokeai/backend/stable_diffusion/denoise_context.py b/invokeai/backend/stable_diffusion/denoise_context.py new file mode 100644 index 00000000000..9060d549776 --- /dev/null +++ b/invokeai/backend/stable_diffusion/denoise_context.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Type, Union + +import torch +from diffusers import UNet2DConditionModel +from diffusers.schedulers.scheduling_utils import SchedulerMixin, SchedulerOutput + +if TYPE_CHECKING: + from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningMode, TextConditioningData + + +@dataclass +class UNetKwargs: + sample: torch.Tensor + timestep: Union[torch.Tensor, float, int] + encoder_hidden_states: torch.Tensor + + class_labels: Optional[torch.Tensor] = None + timestep_cond: Optional[torch.Tensor] = None + attention_mask: Optional[torch.Tensor] = None + cross_attention_kwargs: Optional[Dict[str, Any]] = None + added_cond_kwargs: Optional[Dict[str, torch.Tensor]] = None + down_block_additional_residuals: Optional[Tuple[torch.Tensor]] = None + mid_block_additional_residual: Optional[torch.Tensor] = None + down_intrablock_additional_residuals: Optional[Tuple[torch.Tensor]] = None + encoder_attention_mask: Optional[torch.Tensor] = None + # return_dict: bool = True + + +@dataclass +class DenoiseInputs: + """Initial variables passed to denoise. Supposed to be unchanged.""" + + # The latent-space image to denoise. + # Shape: [batch, channels, latent_height, latent_width] + # - If we are inpainting, this is the initial latent image before noise has been added. + # - If we are generating a new image, this should be initialized to zeros. + # - In some cases, this may be a partially-noised latent image (e.g. when running the SDXL refiner). + orig_latents: torch.Tensor + + # kwargs forwarded to the scheduler.step() method. + scheduler_step_kwargs: dict[str, Any] + + # Text conditionging data. + conditioning_data: TextConditioningData + + # Noise used for two purposes: + # 1. Used by the scheduler to noise the initial `latents` before denoising. + # 2. Used to noise the `masked_latents` when inpainting. + # `noise` should be None if the `latents` tensor has already been noised. + # Shape: [1 or batch, channels, latent_height, latent_width] + noise: Optional[torch.Tensor] + + # The seed used to generate the noise for the denoising process. + # HACK(ryand): seed is only used in a particular case when `noise` is None, but we need to re-generate the + # same noise used earlier in the pipeline. This should really be handled in a clearer way. + seed: int + + # The timestep schedule for the denoising process. + timesteps: torch.Tensor + + # The first timestep in the schedule. This is used to determine the initial noise level, so + # should be populated if you want noise applied *even* if timesteps is empty. + init_timestep: torch.Tensor + + # Class of attention processor that is used. + attention_processor_cls: Type[Any] + + +@dataclass +class DenoiseContext: + """Context with all variables in denoise""" + + # Initial variables passed to denoise. Supposed to be unchanged. + inputs: DenoiseInputs + + # Scheduler which used to apply noise predictions. + scheduler: SchedulerMixin + + # UNet model. + unet: Optional[UNet2DConditionModel] = None + + # Current state of latent-space image in denoising process. + # None until `PRE_DENOISE_LOOP` callback. + # Shape: [batch, channels, latent_height, latent_width] + latents: Optional[torch.Tensor] = None + + # Current denoising step index. + # None until `PRE_STEP` callback. + step_index: Optional[int] = None + + # Current denoising step timestep. + # None until `PRE_STEP` callback. + timestep: Optional[torch.Tensor] = None + + # Arguments which will be passed to UNet model. + # Available in `PRE_UNET`/`POST_UNET` callbacks, otherwise will be None. + unet_kwargs: Optional[UNetKwargs] = None + + # SchedulerOutput class returned from step function(normally, generated by scheduler). + # Supposed to be used only in `POST_STEP` callback, otherwise can be None. + step_output: Optional[SchedulerOutput] = None + + # Scaled version of `latents`, which will be passed to unet_kwargs initialization. + # Available in events inside step(between `PRE_STEP` and `POST_STEP`). + # Shape: [batch, channels, latent_height, latent_width] + latent_model_input: Optional[torch.Tensor] = None + + # [TMP] Defines on which conditionings current unet call will be runned. + # Available in `PRE_UNET`/`POST_UNET` callbacks, otherwise will be None. + conditioning_mode: Optional[ConditioningMode] = None + + # [TMP] Noise predictions from negative conditioning. + # Available in `POST_COMBINE_NOISE_PREDS` callback, otherwise will be None. + # Shape: [batch, channels, latent_height, latent_width] + negative_noise_pred: Optional[torch.Tensor] = None + + # [TMP] Noise predictions from positive conditioning. + # Available in `POST_COMBINE_NOISE_PREDS` callback, otherwise will be None. + # Shape: [batch, channels, latent_height, latent_width] + positive_noise_pred: Optional[torch.Tensor] = None + + # Combined noise prediction from passed conditionings. + # Available in `POST_COMBINE_NOISE_PREDS` callback, otherwise will be None. + # Shape: [batch, channels, latent_height, latent_width] + noise_pred: Optional[torch.Tensor] = None + + # Dictionary for extensions to pass extra info about denoise process to other extensions. + extra: dict = field(default_factory=dict) diff --git a/invokeai/backend/stable_diffusion/diffusers_pipeline.py b/invokeai/backend/stable_diffusion/diffusers_pipeline.py new file mode 100644 index 00000000000..054e04dcb28 --- /dev/null +++ b/invokeai/backend/stable_diffusion/diffusers_pipeline.py @@ -0,0 +1,617 @@ +from __future__ import annotations + +import math +from contextlib import nullcontext +from dataclasses import dataclass +from typing import Any, Callable, List, Optional, Union + +import einops +import PIL.Image +import psutil +import torch +import torchvision.transforms as T +from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL +from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel +from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion import StableDiffusionPipeline +from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from diffusers.schedulers.scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin +from diffusers.utils.import_utils import is_xformers_available +from pydantic import Field +from transformers import CLIPImageProcessor, CLIPTextModel, CLIPTokenizer + +from invokeai.app.services.config.config_default import get_config +from invokeai.backend.stable_diffusion.diffusion.conditioning_data import IPAdapterData, TextConditioningData +from invokeai.backend.stable_diffusion.diffusion.shared_invokeai_diffusion import InvokeAIDiffuserComponent +from invokeai.backend.stable_diffusion.diffusion.unet_attention_patcher import UNetAttentionPatcher, UNetIPAdapterData +from invokeai.backend.stable_diffusion.extensions.preview import PipelineIntermediateState +from invokeai.backend.util.attention import auto_detect_slice_size +from invokeai.backend.util.devices import TorchDevice +from invokeai.backend.util.hotfixes import ControlNetModel + + +@dataclass +class AddsMaskGuidance: + mask: torch.Tensor + mask_latents: torch.Tensor + scheduler: SchedulerMixin + noise: torch.Tensor + is_gradient_mask: bool + + def __call__(self, latents: torch.Tensor, t: torch.Tensor) -> torch.Tensor: + return self.apply_mask(latents, t) + + def apply_mask(self, latents: torch.Tensor, t: torch.Tensor) -> torch.Tensor: + batch_size = latents.size(0) + mask = einops.repeat(self.mask, "b c h w -> (repeat b) c h w", repeat=batch_size) + if t.dim() == 0: + # some schedulers expect t to be one-dimensional. + # TODO: file diffusers bug about inconsistency? + t = einops.repeat(t, "-> batch", batch=batch_size) + # Noise shouldn't be re-randomized between steps here. The multistep schedulers + # get very confused about what is happening from step to step when we do that. + mask_latents = self.scheduler.add_noise(self.mask_latents, self.noise, t) + # TODO: Do we need to also apply scheduler.scale_model_input? Or is add_noise appropriately scaled already? + # mask_latents = self.scheduler.scale_model_input(mask_latents, t) + mask_latents = einops.repeat(mask_latents, "b c h w -> (repeat b) c h w", repeat=batch_size) + if self.is_gradient_mask: + threshhold = (t.item()) / self.scheduler.config.num_train_timesteps + mask_bool = mask > threshhold # I don't know when mask got inverted, but it did + masked_input = torch.where(mask_bool, latents, mask_latents) + else: + masked_input = torch.lerp(mask_latents.to(dtype=latents.dtype), latents, mask.to(dtype=latents.dtype)) + return masked_input + + +def trim_to_multiple_of(*args, multiple_of=8): + return tuple((x - x % multiple_of) for x in args) + + +def image_resized_to_grid_as_tensor(image: PIL.Image.Image, normalize: bool = True, multiple_of=8) -> torch.FloatTensor: + """ + + :param image: input image + :param normalize: scale the range to [-1, 1] instead of [0, 1] + :param multiple_of: resize the input so both dimensions are a multiple of this + """ + w, h = trim_to_multiple_of(*image.size, multiple_of=multiple_of) + transformation = T.Compose( + [ + T.Resize((h, w), T.InterpolationMode.LANCZOS, antialias=True), + T.ToTensor(), + ] + ) + tensor = transformation(image) + if normalize: + tensor = tensor * 2.0 - 1.0 + return tensor + + +def is_inpainting_model(unet: UNet2DConditionModel): + return unet.conv_in.in_channels == 9 + + +@dataclass +class ControlNetData: + model: ControlNetModel = Field(default=None) + image_tensor: torch.Tensor = Field(default=None) + weight: Union[float, List[float]] = Field(default=1.0) + begin_step_percent: float = Field(default=0.0) + end_step_percent: float = Field(default=1.0) + control_mode: str = Field(default="balanced") + resize_mode: str = Field(default="just_resize") + + +@dataclass +class T2IAdapterData: + """A structure containing the information required to apply conditioning from a single T2I-Adapter model.""" + + adapter_state: dict[torch.Tensor] = Field() + weight: Union[float, list[float]] = Field(default=1.0) + begin_step_percent: float = Field(default=0.0) + end_step_percent: float = Field(default=1.0) + + +class StableDiffusionGeneratorPipeline(StableDiffusionPipeline): + r""" + Pipeline for text-to-image generation using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Implementation note: This class started as a refactored copy of diffusers.StableDiffusionPipeline. + Hopefully future versions of diffusers provide access to more of these functions so that we don't + need to duplicate them here: https://github.com/huggingface/diffusers/issues/551#issuecomment-1281508384 + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/CompVis/stable-diffusion-v1-4) for details. + feature_extractor ([`CLIPImageProcessor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: KarrasDiffusionSchedulers, + safety_checker: Optional[StableDiffusionSafetyChecker], + feature_extractor: Optional[CLIPImageProcessor], + requires_safety_checker: bool = False, + ): + super().__init__( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + requires_safety_checker=requires_safety_checker, + ) + + self.invokeai_diffuser = InvokeAIDiffuserComponent(self.unet, self._unet_forward) + + def _adjust_memory_efficient_attention(self, latents: torch.Tensor): + """ + if xformers is available, use it, otherwise use sliced attention. + """ + + # On 30xx and 40xx series GPUs, `torch-sdp` is faster than `xformers`. This corresponds to a CUDA major + # version of 8 or higher. So, for major version 7 or below, we prefer `xformers`. + # See: + # - https://developer.nvidia.com/cuda-gpus + # - https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#compute-capabilities + try: + prefer_xformers = torch.cuda.is_available() and torch.cuda.get_device_properties("cuda").major <= 7 # type: ignore # Type of "get_device_properties" is partially unknown + except Exception: + prefer_xformers = False + + config = get_config() + if config.attention_type == "xformers" and is_xformers_available() and prefer_xformers: + self.enable_xformers_memory_efficient_attention() + return + elif config.attention_type == "sliced": + slice_size = config.attention_slice_size + if slice_size == "auto": + slice_size = auto_detect_slice_size(latents) + elif slice_size == "balanced": + slice_size = "auto" + self.enable_attention_slicing(slice_size=slice_size) + return + elif config.attention_type == "normal": + self.disable_attention_slicing() + return + elif config.attention_type == "torch-sdp": + # torch-sdp is the default in diffusers. + return + + # See https://github.com/invoke-ai/InvokeAI/issues/7049 for context. + # Bumping torch from 2.2.2 to 2.4.1 caused the sliced attention implementation to produce incorrect results. + # For now, if a user is on an MPS device and has not explicitly set the attention_type, then we select the + # non-sliced torch-sdp implementation. This keeps things working on MPS at the cost of increased peak memory + # utilization. + if torch.backends.mps.is_available(): + return + + # The remainder if this code is called when attention_type=='auto'. + if self.unet.device.type == "cuda": + if is_xformers_available() and prefer_xformers: + self.enable_xformers_memory_efficient_attention() + return + # torch-sdp is the default in diffusers. + return + + if self.unet.device.type == "cpu" or self.unet.device.type == "mps": + mem_free = psutil.virtual_memory().free + elif self.unet.device.type == "cuda": + mem_free, _ = torch.cuda.mem_get_info(TorchDevice.normalize(self.unet.device)) + else: + raise ValueError(f"unrecognized device {self.unet.device}") + # input tensor of [1, 4, h/8, w/8] + # output tensor of [16, (h/8 * w/8), (h/8 * w/8)] + bytes_per_element_needed_for_baddbmm_duplication = latents.element_size() + 4 + max_size_required_for_baddbmm = ( + 16 + * latents.size(dim=2) + * latents.size(dim=3) + * latents.size(dim=2) + * latents.size(dim=3) + * bytes_per_element_needed_for_baddbmm_duplication + ) + if max_size_required_for_baddbmm > (mem_free * 3.0 / 4.0): # 3.3 / 4.0 is from old Invoke code + self.enable_attention_slicing(slice_size="max") + elif torch.backends.mps.is_available(): + # diffusers recommends always enabling for mps + self.enable_attention_slicing(slice_size="max") + else: + self.disable_attention_slicing() + + def to(self, torch_device: Optional[Union[str, torch.device]] = None, silence_dtype_warnings=False): + raise Exception("Should not be called") + + def add_inpainting_channels_to_latents( + self, latents: torch.Tensor, masked_ref_image_latents: torch.Tensor, inpainting_mask: torch.Tensor + ): + """Given a `latents` tensor, adds the mask and image latents channels required for inpainting. + + Standard (non-inpainting) SD UNet models expect an input with shape (N, 4, H, W). Inpainting models expect an + input of shape (N, 9, H, W). The 9 channels are defined as follows: + - Channel 0-3: The latents being denoised. + - Channel 4: The mask indicating which parts of the image are being inpainted. + - Channel 5-8: The latent representation of the masked reference image being inpainted. + + This function assumes that the same mask and base image should apply to all items in the batch. + """ + # Validate assumptions about input tensor shapes. + batch_size, latent_channels, latent_height, latent_width = latents.shape + assert latent_channels == 4 + assert list(masked_ref_image_latents.shape) == [1, 4, latent_height, latent_width] + assert list(inpainting_mask.shape) == [1, 1, latent_height, latent_width] + + # Repeat original_image_latents and inpainting_mask to match the latents batch size. + original_image_latents = masked_ref_image_latents.expand(batch_size, -1, -1, -1) + inpainting_mask = inpainting_mask.expand(batch_size, -1, -1, -1) + + # Concatenate along the channel dimension. + return torch.cat([latents, inpainting_mask, original_image_latents], dim=1) + + def latents_from_embeddings( + self, + latents: torch.Tensor, + scheduler_step_kwargs: dict[str, Any], + conditioning_data: TextConditioningData, + noise: Optional[torch.Tensor], + seed: int, + timesteps: torch.Tensor, + init_timestep: torch.Tensor, + callback: Callable[[PipelineIntermediateState], None], + control_data: list[ControlNetData] | None = None, + ip_adapter_data: Optional[list[IPAdapterData]] = None, + t2i_adapter_data: Optional[list[T2IAdapterData]] = None, + mask: Optional[torch.Tensor] = None, + masked_latents: Optional[torch.Tensor] = None, + is_gradient_mask: bool = False, + ) -> torch.Tensor: + """Denoise the latents. + + Args: + latents: The latent-space image to denoise. + - If we are inpainting, this is the initial latent image before noise has been added. + - If we are generating a new image, this should be initialized to zeros. + - In some cases, this may be a partially-noised latent image (e.g. when running the SDXL refiner). + scheduler_step_kwargs: kwargs forwarded to the scheduler.step() method. + conditioning_data: Text conditionging data. + noise: Noise used for two purposes: + 1. Used by the scheduler to noise the initial `latents` before denoising. + 2. Used to noise the `masked_latents` when inpainting. + `noise` should be None if the `latents` tensor has already been noised. + seed: The seed used to generate the noise for the denoising process. + HACK(ryand): seed is only used in a particular case when `noise` is None, but we need to re-generate the + same noise used earlier in the pipeline. This should really be handled in a clearer way. + timesteps: The timestep schedule for the denoising process. + init_timestep: The first timestep in the schedule. This is used to determine the initial noise level, so + should be populated if you want noise applied *even* if timesteps is empty. + callback: A callback function that is called to report progress during the denoising process. + control_data: ControlNet data. + ip_adapter_data: IP-Adapter data. + t2i_adapter_data: T2I-Adapter data. + mask: A mask indicating which parts of the image are being inpainted. The presence of mask is used to + determine whether we are inpainting or not. `mask` should have the same spatial dimensions as the + `latents` tensor. + TODO(ryand): Check and document the expected dtype, range, and values used to represent + foreground/background. + masked_latents: A latent-space representation of a masked inpainting reference image. This tensor is only + used if an *inpainting* model is being used i.e. this tensor is not used when inpainting with a standard + SD UNet model. + is_gradient_mask: A flag indicating whether `mask` is a gradient mask or not. + """ + if init_timestep.shape[0] == 0: + return latents + + orig_latents = latents.clone() + + batch_size = latents.shape[0] + batched_init_timestep = init_timestep.expand(batch_size) + + # noise can be None if the latents have already been noised (e.g. when running the SDXL refiner). + if noise is not None: + # TODO(ryand): I'm pretty sure we should be applying init_noise_sigma in cases where we are starting with + # full noise. Investigate the history of why this got commented out. + # latents = noise * self.scheduler.init_noise_sigma # it's like in t2l according to diffusers + latents = self.scheduler.add_noise(latents, noise, batched_init_timestep) + + self._adjust_memory_efficient_attention(latents) + + # Handle mask guidance (a.k.a. inpainting). + mask_guidance: AddsMaskGuidance | None = None + if mask is not None and not is_inpainting_model(self.unet): + # We are doing inpainting, since a mask is provided, but we are not using an inpainting model, so we will + # apply mask guidance to the latents. + + # 'noise' might be None if the latents have already been noised (e.g. when running the SDXL refiner). + # We still need noise for inpainting, so we generate it from the seed here. + if noise is None: + noise = torch.randn( + orig_latents.shape, + dtype=torch.float32, + device="cpu", + generator=torch.Generator(device="cpu").manual_seed(seed), + ).to(device=orig_latents.device, dtype=orig_latents.dtype) + + mask_guidance = AddsMaskGuidance( + mask=mask, + mask_latents=orig_latents, + scheduler=self.scheduler, + noise=noise, + is_gradient_mask=is_gradient_mask, + ) + + use_ip_adapter = ip_adapter_data is not None + use_regional_prompting = ( + conditioning_data.cond_regions is not None or conditioning_data.uncond_regions is not None + ) + unet_attention_patcher = None + attn_ctx = nullcontext() + + if use_ip_adapter or use_regional_prompting: + ip_adapters: Optional[List[UNetIPAdapterData]] = ( + [ + {"ip_adapter": ipa.ip_adapter_model, "target_blocks": ipa.target_blocks, "method": ipa.method} + for ipa in ip_adapter_data + ] + if use_ip_adapter + else None + ) + unet_attention_patcher = UNetAttentionPatcher(ip_adapters) + attn_ctx = unet_attention_patcher.apply_ip_adapter_attention(self.invokeai_diffuser.model) + + with attn_ctx: + callback( + PipelineIntermediateState( + step=0, # initial latents + order=self.scheduler.order, + total_steps=len(timesteps), + timestep=self.scheduler.config.num_train_timesteps, + latents=latents, + ) + ) + + for i, t in enumerate(self.progress_bar(timesteps)): + batched_t = t.expand(batch_size) + step_output = self.step( + t=batched_t, + latents=latents, + conditioning_data=conditioning_data, + step_index=i, + total_step_count=len(timesteps), + scheduler_step_kwargs=scheduler_step_kwargs, + mask_guidance=mask_guidance, + mask=mask, + masked_latents=masked_latents, + control_data=control_data, + ip_adapter_data=ip_adapter_data, + t2i_adapter_data=t2i_adapter_data, + ) + latents = step_output.prev_sample + predicted_original = getattr(step_output, "pred_original_sample", None) + + callback( + PipelineIntermediateState( + step=i + 1, # final latents + order=self.scheduler.order, + total_steps=len(timesteps), + timestep=int(t), + latents=latents, + predicted_original=predicted_original, + ) + ) + + # restore unmasked part after the last step is completed + # in-process masking happens before each step + if mask is not None: + if is_gradient_mask: + latents = torch.where(mask > 0, latents, orig_latents) + else: + latents = torch.lerp( + orig_latents, latents.to(dtype=orig_latents.dtype), mask.to(dtype=orig_latents.dtype) + ) + + return latents + + @torch.inference_mode() + def step( + self, + t: torch.Tensor, + latents: torch.Tensor, + conditioning_data: TextConditioningData, + step_index: int, + total_step_count: int, + scheduler_step_kwargs: dict[str, Any], + mask_guidance: AddsMaskGuidance | None, + mask: torch.Tensor | None, + masked_latents: torch.Tensor | None, + control_data: list[ControlNetData] | None = None, + ip_adapter_data: Optional[list[IPAdapterData]] = None, + t2i_adapter_data: Optional[list[T2IAdapterData]] = None, + ): + # invokeai_diffuser has batched timesteps, but diffusers schedulers expect a single value + timestep = t[0] + + # Handle masked image-to-image (a.k.a inpainting). + if mask_guidance is not None: + # NOTE: This is intentionally done *before* self.scheduler.scale_model_input(...). + latents = mask_guidance(latents, timestep) + + # TODO: should this scaling happen here or inside self._unet_forward? + # i.e. before or after passing it to InvokeAIDiffuserComponent + latent_model_input = self.scheduler.scale_model_input(latents, timestep) + + # Handle ControlNet(s) + down_block_additional_residuals = None + mid_block_additional_residual = None + if control_data is not None: + down_block_additional_residuals, mid_block_additional_residual = self.invokeai_diffuser.do_controlnet_step( + control_data=control_data, + sample=latent_model_input, + timestep=timestep, + step_index=step_index, + total_step_count=total_step_count, + conditioning_data=conditioning_data, + ) + + # Handle T2I-Adapter(s) + down_intrablock_additional_residuals = None + if t2i_adapter_data is not None: + accum_adapter_state = None + for single_t2i_adapter_data in t2i_adapter_data: + # Determine the T2I-Adapter weights for the current denoising step. + first_t2i_adapter_step = math.floor(single_t2i_adapter_data.begin_step_percent * total_step_count) + last_t2i_adapter_step = math.ceil(single_t2i_adapter_data.end_step_percent * total_step_count) + t2i_adapter_weight = ( + single_t2i_adapter_data.weight[step_index] + if isinstance(single_t2i_adapter_data.weight, list) + else single_t2i_adapter_data.weight + ) + if step_index < first_t2i_adapter_step or step_index > last_t2i_adapter_step: + # If the current step is outside of the T2I-Adapter's begin/end step range, then set its weight to 0 + # so it has no effect. + t2i_adapter_weight = 0.0 + + # Apply the t2i_adapter_weight, and accumulate. + if accum_adapter_state is None: + # Handle the first T2I-Adapter. + accum_adapter_state = [val * t2i_adapter_weight for val in single_t2i_adapter_data.adapter_state] + else: + # Add to the previous adapter states. + for idx, value in enumerate(single_t2i_adapter_data.adapter_state): + accum_adapter_state[idx] += value * t2i_adapter_weight + + # Hack: force compatibility with irregular resolutions by padding the feature map with zeros + for idx, tensor in enumerate(accum_adapter_state): + # The tensor size is supposed to be some integer downscale factor of the latents size. + # Internally, the unet will pad the latents before downscaling between levels when it is no longer divisible by its downscale factor. + # If the latent size does not scale down evenly, we need to pad the tensor so that it matches the the downscaled padded latents later on. + scale_factor = latents.size()[-1] // tensor.size()[-1] + required_padding_width = math.ceil(latents.size()[-1] / scale_factor) - tensor.size()[-1] + required_padding_height = math.ceil(latents.size()[-2] / scale_factor) - tensor.size()[-2] + tensor = torch.nn.functional.pad( + tensor, + (0, required_padding_width, 0, required_padding_height, 0, 0, 0, 0), + mode="constant", + value=0, + ) + accum_adapter_state[idx] = tensor + + down_intrablock_additional_residuals = accum_adapter_state + + # Handle inpainting models. + if is_inpainting_model(self.unet): + # NOTE: These calls to add_inpainting_channels_to_latents(...) are intentionally done *after* + # self.scheduler.scale_model_input(...) so that the scaling is not applied to the mask or reference image + # latents. + if mask is not None: + if masked_latents is None: + raise ValueError("Source image required for inpaint mask when inpaint model used!") + latent_model_input = self.add_inpainting_channels_to_latents( + latents=latent_model_input, masked_ref_image_latents=masked_latents, inpainting_mask=mask + ) + else: + # We are using an inpainting model, but no mask was provided, so we are not really "inpainting". + # We generate a global mask and empty original image so that we can still generate in this + # configuration. + # TODO(ryand): Should we just raise an exception here instead? I can't think of a use case for wanting + # to do this. + # TODO(ryand): If we decide that there is a good reason to keep this, then we should generate the 'fake' + # mask and original image once rather than on every denoising step. + latent_model_input = self.add_inpainting_channels_to_latents( + latents=latent_model_input, + masked_ref_image_latents=torch.zeros_like(latent_model_input[:1]), + inpainting_mask=torch.ones_like(latent_model_input[:1, :1]), + ) + + uc_noise_pred, c_noise_pred = self.invokeai_diffuser.do_unet_step( + sample=latent_model_input, + timestep=t, # TODO: debug how handled batched and non batched timesteps + step_index=step_index, + total_step_count=total_step_count, + conditioning_data=conditioning_data, + ip_adapter_data=ip_adapter_data, + down_block_additional_residuals=down_block_additional_residuals, # for ControlNet + mid_block_additional_residual=mid_block_additional_residual, # for ControlNet + down_intrablock_additional_residuals=down_intrablock_additional_residuals, # for T2I-Adapter + ) + + guidance_scale = conditioning_data.guidance_scale + if isinstance(guidance_scale, list): + guidance_scale = guidance_scale[step_index] + + noise_pred = self.invokeai_diffuser._combine(uc_noise_pred, c_noise_pred, guidance_scale) + guidance_rescale_multiplier = conditioning_data.guidance_rescale_multiplier + if guidance_rescale_multiplier > 0: + noise_pred = self._rescale_cfg( + noise_pred, + c_noise_pred, + guidance_rescale_multiplier, + ) + + # compute the previous noisy sample x_t -> x_t-1 + step_output = self.scheduler.step(noise_pred, timestep, latents, **scheduler_step_kwargs) + + # TODO: discuss injection point options. For now this is a patch to get progress images working with inpainting + # again. + if mask_guidance is not None: + # Apply the mask to any "denoised" or "pred_original_sample" fields. + if hasattr(step_output, "denoised"): + step_output.pred_original_sample = mask_guidance(step_output.denoised, self.scheduler.timesteps[-1]) + elif hasattr(step_output, "pred_original_sample"): + step_output.pred_original_sample = mask_guidance( + step_output.pred_original_sample, self.scheduler.timesteps[-1] + ) + else: + step_output.pred_original_sample = mask_guidance(latents, self.scheduler.timesteps[-1]) + + return step_output + + @staticmethod + def _rescale_cfg(total_noise_pred, pos_noise_pred, multiplier=0.7): + """Implementation of Algorithm 2 from https://arxiv.org/pdf/2305.08891.pdf.""" + ro_pos = torch.std(pos_noise_pred, dim=(1, 2, 3), keepdim=True) + ro_cfg = torch.std(total_noise_pred, dim=(1, 2, 3), keepdim=True) + + x_rescaled = total_noise_pred * (ro_pos / ro_cfg) + x_final = multiplier * x_rescaled + (1.0 - multiplier) * total_noise_pred + return x_final + + def _unet_forward( + self, + latents, + t, + text_embeddings, + cross_attention_kwargs: Optional[dict[str, Any]] = None, + **kwargs, + ): + """predict the noise residual""" + # First three args should be positional, not keywords, so torch hooks can see them. + return self.unet( + latents, + t, + text_embeddings, + cross_attention_kwargs=cross_attention_kwargs, + **kwargs, + ).sample diff --git a/invokeai/backend/stable_diffusion/diffusion/__init__.py b/invokeai/backend/stable_diffusion/diffusion/__init__.py new file mode 100644 index 00000000000..712542f79cf --- /dev/null +++ b/invokeai/backend/stable_diffusion/diffusion/__init__.py @@ -0,0 +1,7 @@ +""" +Initialization file for invokeai.models.diffusion +""" + +from invokeai.backend.stable_diffusion.diffusion.shared_invokeai_diffusion import ( + InvokeAIDiffuserComponent, # noqa: F401 +) diff --git a/invokeai/backend/stable_diffusion/diffusion/conditioning_data.py b/invokeai/backend/stable_diffusion/diffusion/conditioning_data.py new file mode 100644 index 00000000000..6a9959f1e87 --- /dev/null +++ b/invokeai/backend/stable_diffusion/diffusion/conditioning_data.py @@ -0,0 +1,366 @@ +from __future__ import annotations + +import math +from dataclasses import dataclass, field +from enum import Enum +from typing import TYPE_CHECKING, List, Optional, Tuple, Union + +import torch + +from invokeai.backend.stable_diffusion.diffusion.regional_prompt_data import RegionalPromptData + +if TYPE_CHECKING: + from invokeai.backend.ip_adapter.ip_adapter import IPAdapter + from invokeai.backend.stable_diffusion.denoise_context import UNetKwargs + + +@dataclass +class BasicConditioningInfo: + """SD 1/2 text conditioning information produced by Compel.""" + + embeds: torch.Tensor + + def to(self, device, dtype=None): + self.embeds = self.embeds.to(device=device, dtype=dtype) + return self + + +@dataclass +class SDXLConditioningInfo(BasicConditioningInfo): + """SDXL text conditioning information produced by Compel.""" + + pooled_embeds: torch.Tensor + add_time_ids: torch.Tensor + + def to(self, device, dtype=None): + self.pooled_embeds = self.pooled_embeds.to(device=device, dtype=dtype) + self.add_time_ids = self.add_time_ids.to(device=device, dtype=dtype) + return super().to(device=device, dtype=dtype) + + +@dataclass +class FLUXConditioningInfo: + clip_embeds: torch.Tensor + t5_embeds: torch.Tensor + + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + self.clip_embeds = self.clip_embeds.to(device=device, dtype=dtype) + self.t5_embeds = self.t5_embeds.to(device=device, dtype=dtype) + return self + + +@dataclass +class SD3ConditioningInfo: + clip_l_pooled_embeds: torch.Tensor + clip_l_embeds: torch.Tensor + clip_g_pooled_embeds: torch.Tensor + clip_g_embeds: torch.Tensor + t5_embeds: torch.Tensor | None + + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + self.clip_l_pooled_embeds = self.clip_l_pooled_embeds.to(device=device, dtype=dtype) + self.clip_l_embeds = self.clip_l_embeds.to(device=device, dtype=dtype) + self.clip_g_pooled_embeds = self.clip_g_pooled_embeds.to(device=device, dtype=dtype) + self.clip_g_embeds = self.clip_g_embeds.to(device=device, dtype=dtype) + if self.t5_embeds is not None: + self.t5_embeds = self.t5_embeds.to(device=device, dtype=dtype) + return self + + +@dataclass +class CogView4ConditioningInfo: + glm_embeds: torch.Tensor + + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + self.glm_embeds = self.glm_embeds.to(device=device, dtype=dtype) + return self + + +@dataclass +class ZImageConditioningInfo: + """Z-Image text conditioning information from Qwen3 text encoder.""" + + prompt_embeds: torch.Tensor + """Text embeddings from Qwen3 encoder. Shape: (batch_size, seq_len, hidden_size).""" + + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + self.prompt_embeds = self.prompt_embeds.to(device=device, dtype=dtype) + return self + + +@dataclass +class QwenImageConditioningInfo: + """Qwen Image Edit conditioning information from Qwen2.5-VL encoder.""" + + prompt_embeds: torch.Tensor + """Text/image embeddings from Qwen2.5-VL encoder. Shape: (batch_size, seq_len, hidden_size).""" + + prompt_embeds_mask: torch.Tensor | None = None + """Attention mask for prompt_embeds. Shape: (batch_size, seq_len). 1 for valid, 0 for padding.""" + + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + self.prompt_embeds = self.prompt_embeds.to(device=device, dtype=dtype) + if self.prompt_embeds_mask is not None: + self.prompt_embeds_mask = self.prompt_embeds_mask.to(device=device) + return self + + +@dataclass +class AnimaConditioningInfo: + """Anima text conditioning information from Qwen3 0.6B encoder + T5-XXL tokenizer. + + Anima uses a dual-conditioning scheme where Qwen3 hidden states are combined + with T5-XXL token IDs inside the LLM Adapter (part of the transformer). + """ + + qwen3_embeds: torch.Tensor + """Qwen3 0.6B hidden states. Shape: (seq_len, hidden_size) where hidden_size=1024.""" + + t5xxl_ids: torch.Tensor + """T5-XXL token IDs. Shape: (seq_len,).""" + + t5xxl_weights: Optional[torch.Tensor] = None + """Per-token weights for prompt weighting. Shape: (seq_len,). None means uniform weight.""" + + def to(self, device: torch.device | None = None, dtype: torch.dtype | None = None): + self.qwen3_embeds = self.qwen3_embeds.to(device=device, dtype=dtype) + self.t5xxl_ids = self.t5xxl_ids.to(device=device) + if self.t5xxl_weights is not None: + self.t5xxl_weights = self.t5xxl_weights.to(device=device, dtype=dtype) + return self + + +@dataclass +class ConditioningFieldData: + # If you change this class, adding more types, you _must_ update the instantiation of ObjectSerializerDisk in + # invokeai/app/api/dependencies.py, adding the types to the list of safe globals. If you do not, torch will be + # unable to deserialize the object and will raise an error. + conditionings: ( + List[BasicConditioningInfo] + | List[SDXLConditioningInfo] + | List[FLUXConditioningInfo] + | List[SD3ConditioningInfo] + | List[CogView4ConditioningInfo] + | List[ZImageConditioningInfo] + | List[QwenImageConditioningInfo] + | List[AnimaConditioningInfo] + ) + + +@dataclass +class IPAdapterConditioningInfo: + cond_image_prompt_embeds: torch.Tensor + """IP-Adapter image encoder conditioning embeddings. + Shape: (num_images, num_tokens, encoding_dim). + """ + uncond_image_prompt_embeds: torch.Tensor + """IP-Adapter image encoding embeddings to use for unconditional generation. + Shape: (num_images, num_tokens, encoding_dim). + """ + + +@dataclass +class IPAdapterData: + """Data class for IP-Adapter configuration. + + Attributes: + ip_adapter_model: The IP-Adapter model to use. + ip_adapter_conditioning: The IP-Adapter conditioning data. + mask: The mask to apply to the IP-Adapter conditioning. + target_blocks: List of target attention block names to apply IP-Adapter to. + negative_blocks: List of target attention block names that should use negative attention. + weight: The weight to apply to the IP-Adapter conditioning. + begin_step_percent: The percentage of steps at which to start applying the IP-Adapter. + end_step_percent: The percentage of steps at which to stop applying the IP-Adapter. + method: The method to use for applying the IP-Adapter ('full', 'style', 'composition'). + """ + + ip_adapter_model: IPAdapter + ip_adapter_conditioning: IPAdapterConditioningInfo + mask: torch.Tensor + target_blocks: List[str] + negative_blocks: List[str] = field(default_factory=list) + weight: Union[float, List[float]] = 1.0 + begin_step_percent: float = 0.0 + end_step_percent: float = 1.0 + method: str = "full" + + def scale_for_step(self, step_index: int, total_steps: int) -> float: + first_adapter_step = math.floor(self.begin_step_percent * total_steps) + last_adapter_step = math.ceil(self.end_step_percent * total_steps) + weight = self.weight[step_index] if isinstance(self.weight, List) else self.weight + if step_index >= first_adapter_step and step_index <= last_adapter_step: + # Only apply this IP-Adapter if the current step is within the IP-Adapter's begin/end step range. + return weight + # Otherwise, set the IP-Adapter's scale to 0, so it has no effect. + return 0.0 + + +@dataclass +class Range: + start: int + end: int + + +class TextConditioningRegions: + def __init__( + self, + masks: torch.Tensor, + ranges: list[Range], + ): + # A binary mask indicating the regions of the image that the prompt should be applied to. + # Shape: (1, num_prompts, height, width) + # Dtype: torch.bool + self.masks = masks + + # A list of ranges indicating the start and end indices of the embeddings that corresponding mask applies to. + # ranges[i] contains the embedding range for the i'th prompt / mask. + self.ranges = ranges + + assert self.masks.shape[1] == len(self.ranges) + + +class ConditioningMode(Enum): + Both = "both" + Negative = "negative" + Positive = "positive" + + +class TextConditioningData: + def __init__( + self, + uncond_text: Union[BasicConditioningInfo, SDXLConditioningInfo], + cond_text: Union[BasicConditioningInfo, SDXLConditioningInfo], + uncond_regions: Optional[TextConditioningRegions], + cond_regions: Optional[TextConditioningRegions], + guidance_scale: Union[float, List[float]], + guidance_rescale_multiplier: float = 0, # TODO: old backend, remove + ): + self.uncond_text = uncond_text + self.cond_text = cond_text + self.uncond_regions = uncond_regions + self.cond_regions = cond_regions + # Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + # `guidance_scale` is defined as `w` of equation 2. of [Imagen Paper](https://arxiv.org/pdf/2205.11487.pdf). + # Guidance scale is enabled by setting `guidance_scale > 1`. Higher guidance scale encourages to generate + # images that are closely linked to the text `prompt`, usually at the expense of lower image quality. + self.guidance_scale = guidance_scale + # TODO: old backend, remove + # For models trained using zero-terminal SNR ("ztsnr"), it's suggested to use guidance_rescale_multiplier of 0.7. + # See [Common Diffusion Noise Schedules and Sample Steps are Flawed](https://arxiv.org/pdf/2305.08891.pdf). + self.guidance_rescale_multiplier = guidance_rescale_multiplier + + def is_sdxl(self): + assert isinstance(self.uncond_text, SDXLConditioningInfo) == isinstance(self.cond_text, SDXLConditioningInfo) + return isinstance(self.cond_text, SDXLConditioningInfo) + + def to_unet_kwargs(self, unet_kwargs: UNetKwargs, conditioning_mode: ConditioningMode): + """Fills unet arguments with data from provided conditionings. + + Args: + unet_kwargs (UNetKwargs): Object which stores UNet model arguments. + conditioning_mode (ConditioningMode): Describes which conditionings should be used. + """ + _, _, h, w = unet_kwargs.sample.shape + device = unet_kwargs.sample.device + dtype = unet_kwargs.sample.dtype + + # TODO: combine regions with conditionings + if conditioning_mode == ConditioningMode.Both: + conditionings = [self.uncond_text, self.cond_text] + c_regions = [self.uncond_regions, self.cond_regions] + elif conditioning_mode == ConditioningMode.Positive: + conditionings = [self.cond_text] + c_regions = [self.cond_regions] + elif conditioning_mode == ConditioningMode.Negative: + conditionings = [self.uncond_text] + c_regions = [self.uncond_regions] + else: + raise ValueError(f"Unexpected conditioning mode: {conditioning_mode}") + + encoder_hidden_states, encoder_attention_mask = self._concat_conditionings_for_batch( + [c.embeds for c in conditionings] + ) + + unet_kwargs.encoder_hidden_states = encoder_hidden_states + unet_kwargs.encoder_attention_mask = encoder_attention_mask + + if self.is_sdxl(): + added_cond_kwargs = dict( # noqa: C408 + text_embeds=torch.cat([c.pooled_embeds for c in conditionings]), + time_ids=torch.cat([c.add_time_ids for c in conditionings]), + ) + + unet_kwargs.added_cond_kwargs = added_cond_kwargs + + if any(r is not None for r in c_regions): + tmp_regions = [] + for c, r in zip(conditionings, c_regions, strict=True): + if r is None: + r = TextConditioningRegions( + masks=torch.ones((1, 1, h, w), dtype=dtype), + ranges=[Range(start=0, end=c.embeds.shape[1])], + ) + tmp_regions.append(r) + + if unet_kwargs.cross_attention_kwargs is None: + unet_kwargs.cross_attention_kwargs = {} + + unet_kwargs.cross_attention_kwargs.update( + regional_prompt_data=RegionalPromptData(regions=tmp_regions, device=device, dtype=dtype), + ) + + @staticmethod + def _pad_zeros(t: torch.Tensor, pad_shape: tuple, dim: int) -> torch.Tensor: + return torch.cat([t, torch.zeros(pad_shape, device=t.device, dtype=t.dtype)], dim=dim) + + @classmethod + def _pad_conditioning( + cls, + cond: torch.Tensor, + target_len: int, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Pad provided conditioning tensor to target_len by zeros and returns mask of unpadded bytes. + + Args: + cond (torch.Tensor): Conditioning tensor which to pads by zeros. + target_len (int): To which length(tokens count) pad tensor. + """ + conditioning_attention_mask = torch.ones((cond.shape[0], cond.shape[1]), device=cond.device, dtype=cond.dtype) + + if cond.shape[1] < target_len: + conditioning_attention_mask = cls._pad_zeros( + conditioning_attention_mask, + pad_shape=(cond.shape[0], target_len - cond.shape[1]), + dim=1, + ) + + cond = cls._pad_zeros( + cond, + pad_shape=(cond.shape[0], target_len - cond.shape[1], cond.shape[2]), + dim=1, + ) + + return cond, conditioning_attention_mask + + @classmethod + def _concat_conditionings_for_batch( + cls, + conditionings: List[torch.Tensor], + ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]: + """Concatenate provided conditioning tensors to one batched tensor. + If tensors have different sizes then pad them by zeros and creates + encoder_attention_mask to exclude padding from attention. + + Args: + conditionings (List[torch.Tensor]): List of conditioning tensors to concatenate. + """ + encoder_attention_mask = None + max_len = max([c.shape[1] for c in conditionings]) + if any(c.shape[1] != max_len for c in conditionings): + encoder_attention_masks = [None] * len(conditionings) + for i in range(len(conditionings)): + conditionings[i], encoder_attention_masks[i] = cls._pad_conditioning(conditionings[i], max_len) + encoder_attention_mask = torch.cat(encoder_attention_masks) + + return torch.cat(conditionings), encoder_attention_mask diff --git a/invokeai/backend/stable_diffusion/diffusion/custom_atttention.py b/invokeai/backend/stable_diffusion/diffusion/custom_atttention.py new file mode 100644 index 00000000000..d0073ddbff8 --- /dev/null +++ b/invokeai/backend/stable_diffusion/diffusion/custom_atttention.py @@ -0,0 +1,219 @@ +from dataclasses import dataclass +from typing import List, Optional, cast + +import torch +import torch.nn.functional as F +from diffusers.models.attention_processor import Attention, AttnProcessor2_0 + +from invokeai.backend.ip_adapter.ip_attention_weights import IPAttentionProcessorWeights +from invokeai.backend.stable_diffusion.diffusion.regional_ip_data import RegionalIPData +from invokeai.backend.stable_diffusion.diffusion.regional_prompt_data import RegionalPromptData + + +@dataclass +class IPAdapterAttentionWeights: + ip_adapter_weights: IPAttentionProcessorWeights + skip: bool + negative: bool + + +class CustomAttnProcessor2_0(AttnProcessor2_0): + """A custom implementation of AttnProcessor2_0 that supports additional Invoke features. + This implementation is based on + https://github.com/huggingface/diffusers/blame/fcfa270fbd1dc294e2f3a505bae6bcb791d721c3/src/diffusers/models/attention_processor.py#L1204 + Supported custom features: + - IP-Adapter + - Regional prompt attention + """ + + def __init__( + self, + ip_adapter_attention_weights: Optional[List[IPAdapterAttentionWeights]] = None, + ): + """Initialize a CustomAttnProcessor2_0. + Note: Arguments that are the same for all attention layers are passed to __call__(). Arguments that are + layer-specific are passed to __init__(). + Args: + ip_adapter_weights: The IP-Adapter attention weights. ip_adapter_weights[i] contains the attention weights + for the i'th IP-Adapter. + """ + super().__init__() + self._ip_adapter_attention_weights = ip_adapter_attention_weights + + def __call__( + self, + attn: Attention, + hidden_states: torch.Tensor, + encoder_hidden_states: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + temb: Optional[torch.Tensor] = None, + # For Regional Prompting: + regional_prompt_data: Optional[RegionalPromptData] = None, + percent_through: Optional[torch.Tensor] = None, + # For IP-Adapter: + regional_ip_data: Optional[RegionalIPData] = None, + *args, + **kwargs, + ) -> torch.FloatTensor: + """Apply attention. + Args: + regional_prompt_data: The regional prompt data for the current batch. If not None, this will be used to + apply regional prompt masking. + regional_ip_data: The IP-Adapter data for the current batch. + """ + # If true, we are doing cross-attention, if false we are doing self-attention. + is_cross_attention = encoder_hidden_states is not None + + # Start unmodified block from AttnProcessor2_0. + # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + residual = hidden_states + if attn.spatial_norm is not None: + hidden_states = attn.spatial_norm(hidden_states, temb) + + input_ndim = hidden_states.ndim + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + # End unmodified block from AttnProcessor2_0. + + _, query_seq_len, _ = hidden_states.shape + # Handle regional prompt attention masks. + if regional_prompt_data is not None and is_cross_attention: + assert percent_through is not None + prompt_region_attention_mask = regional_prompt_data.get_cross_attn_mask( + query_seq_len=query_seq_len, key_seq_len=sequence_length + ) + + if attention_mask is None: + attention_mask = prompt_region_attention_mask + else: + attention_mask = prompt_region_attention_mask + attention_mask + + # Start unmodified block from AttnProcessor2_0. + # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + if attention_mask is not None: + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + # scaled_dot_product_attention expects attention_mask shape to be + # (batch, heads, source_length, target_length) + attention_mask = attention_mask.view(batch_size, attn.heads, -1, attention_mask.shape[-1]) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.norm_cross: + encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + inner_dim = key.shape[-1] + head_dim = inner_dim // attn.heads + + query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + # the output of sdp = (batch, num_heads, seq_len, head_dim) + # TODO: add support for attn.scale when we move to Torch 2.1 + hidden_states = F.scaled_dot_product_attention( + query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False + ) + + hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim) + hidden_states = hidden_states.to(query.dtype) + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + # End unmodified block from AttnProcessor2_0. + + # Apply IP-Adapter conditioning. + if is_cross_attention: + if self._ip_adapter_attention_weights: + assert regional_ip_data is not None + ip_masks = regional_ip_data.get_masks(query_seq_len=query_seq_len) + + assert ( + len(regional_ip_data.image_prompt_embeds) + == len(self._ip_adapter_attention_weights) + == len(regional_ip_data.scales) + == ip_masks.shape[1] + ) + + for ipa_index, ipa_embed in enumerate(regional_ip_data.image_prompt_embeds): + ipa_weights = self._ip_adapter_attention_weights[ipa_index].ip_adapter_weights + ipa_scale = regional_ip_data.scales[ipa_index] + ip_mask = ip_masks[0, ipa_index, ...] + + # The batch dimensions should match. + assert ipa_embed.shape[0] == encoder_hidden_states.shape[0] + # The token_len dimensions should match. + assert ipa_embed.shape[-1] == encoder_hidden_states.shape[-1] + + ip_hidden_states = ipa_embed + + # Expected ip_hidden_state shape: (batch_size, num_ip_images, ip_seq_len, ip_image_embedding) + + if not self._ip_adapter_attention_weights[ipa_index].skip: + # apply the IP-Adapter weights to the negative embeds + if self._ip_adapter_attention_weights[ipa_index].negative: + ip_hidden_states = torch.cat([ip_hidden_states[1], ip_hidden_states[0] * 0], dim=0) + + ip_key = ipa_weights.to_k_ip(ip_hidden_states) + ip_value = ipa_weights.to_v_ip(ip_hidden_states) + + # Expected ip_key and ip_value shape: + # (batch_size, num_ip_images, ip_seq_len, head_dim * num_heads) + + ip_key = ip_key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + ip_value = ip_value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + + # Expected ip_key and ip_value shape: + # (batch_size, num_heads, num_ip_images * ip_seq_len, head_dim) + + # TODO: add support for attn.scale when we move to Torch 2.1 + ip_hidden_states = F.scaled_dot_product_attention( + query, ip_key, ip_value, attn_mask=None, dropout_p=0.0, is_causal=False + ) + + # Expected ip_hidden_states shape: (batch_size, num_heads, query_seq_len, head_dim) + ip_hidden_states = ip_hidden_states.transpose(1, 2).reshape( + batch_size, -1, attn.heads * head_dim + ) + + ip_hidden_states = ip_hidden_states.to(query.dtype) + + # Expected ip_hidden_states shape: (batch_size, query_seq_len, num_heads * head_dim) + hidden_states = hidden_states + ipa_scale * ip_hidden_states * ip_mask + else: + # If IP-Adapter is not enabled, then regional_ip_data should not be passed in. + assert regional_ip_data is None + + # Start unmodified block from AttnProcessor2_0. + # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + # End of unmodified block from AttnProcessor2_0 + + # casting torch.Tensor to torch.FloatTensor to avoid type issues + return cast(torch.FloatTensor, hidden_states) diff --git a/invokeai/backend/stable_diffusion/diffusion/regional_ip_data.py b/invokeai/backend/stable_diffusion/diffusion/regional_ip_data.py new file mode 100644 index 00000000000..792c97114da --- /dev/null +++ b/invokeai/backend/stable_diffusion/diffusion/regional_ip_data.py @@ -0,0 +1,72 @@ +import torch + + +class RegionalIPData: + """A class to manage the data for regional IP-Adapter conditioning.""" + + def __init__( + self, + image_prompt_embeds: list[torch.Tensor], + scales: list[float], + masks: list[torch.Tensor], + dtype: torch.dtype, + device: torch.device, + max_downscale_factor: int = 8, + ): + """Initialize a `IPAdapterConditioningData` object.""" + assert len(image_prompt_embeds) == len(scales) == len(masks) + + # The image prompt embeddings. + # regional_ip_data[i] contains the image prompt embeddings for the i'th IP-Adapter. Each tensor + # has shape (batch_size, num_ip_images, seq_len, ip_embedding_len). + self.image_prompt_embeds = image_prompt_embeds + + # The scales for the IP-Adapter attention. + # scales[i] contains the attention scale for the i'th IP-Adapter. + self.scales = scales + + # The IP-Adapter masks. + # self._masks_by_seq_len[s] contains the spatial masks for the downsampling level with query sequence length of + # s. It has shape (batch_size, num_ip_images, query_seq_len, 1). The masks have values of 1.0 for included + # regions and 0.0 for excluded regions. + self._masks_by_seq_len = self._prepare_masks(masks, max_downscale_factor, device, dtype) + + def _prepare_masks( + self, masks: list[torch.Tensor], max_downscale_factor: int, device: torch.device, dtype: torch.dtype + ) -> dict[int, torch.Tensor]: + """Prepare the masks for the IP-Adapter attention.""" + # Concatenate the masks so that they can be processed more efficiently. + mask_tensor = torch.cat(masks, dim=1) + + mask_tensor = mask_tensor.to(device=device, dtype=dtype) + + masks_by_seq_len: dict[int, torch.Tensor] = {} + + # Downsample the spatial dimensions by factors of 2 until max_downscale_factor is reached. + downscale_factor = 1 + while downscale_factor <= max_downscale_factor: + b, num_ip_adapters, h, w = mask_tensor.shape + # Assert that the batch size is 1, because I haven't thought through batch handling for this feature yet. + assert b == 1 + + # The IP-Adapters are applied in the cross-attention layers, where the query sequence length is the h * w of + # the spatial features. + query_seq_len = h * w + + masks_by_seq_len[query_seq_len] = mask_tensor.view((b, num_ip_adapters, -1, 1)) + + downscale_factor *= 2 + if downscale_factor <= max_downscale_factor: + # We use max pooling because we downscale to a pretty low resolution, so we don't want small mask + # regions to be lost entirely. + # + # ceil_mode=True is set to mirror the downsampling behavior of SD and SDXL. + # + # TODO(ryand): In the future, we may want to experiment with other downsampling methods. + mask_tensor = torch.nn.functional.max_pool2d(mask_tensor, kernel_size=2, stride=2, ceil_mode=True) + + return masks_by_seq_len + + def get_masks(self, query_seq_len: int) -> torch.Tensor: + """Get the mask for the given query sequence length.""" + return self._masks_by_seq_len[query_seq_len] diff --git a/invokeai/backend/stable_diffusion/diffusion/regional_prompt_data.py b/invokeai/backend/stable_diffusion/diffusion/regional_prompt_data.py new file mode 100644 index 00000000000..eddd31f0c42 --- /dev/null +++ b/invokeai/backend/stable_diffusion/diffusion/regional_prompt_data.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch +import torch.nn.functional as F + +if TYPE_CHECKING: + from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ( + TextConditioningRegions, + ) + + +class RegionalPromptData: + """A class to manage the prompt data for regional conditioning.""" + + def __init__( + self, + regions: list[TextConditioningRegions], + device: torch.device, + dtype: torch.dtype, + max_downscale_factor: int = 8, + ): + """Initialize a `RegionalPromptData` object. + Args: + regions (list[TextConditioningRegions]): regions[i] contains the prompt regions for the i'th sample in the + batch. + device (torch.device): The device to use for the attention masks. + dtype (torch.dtype): The data type to use for the attention masks. + max_downscale_factor: Spatial masks will be prepared for downscale factors from 1 to max_downscale_factor + in steps of 2x. + """ + self._regions = regions + self._device = device + self._dtype = dtype + # self._spatial_masks_by_seq_len[b][s] contains the spatial masks for the b'th batch sample with a query + # sequence length of s. + self._spatial_masks_by_seq_len: list[dict[int, torch.Tensor]] = self._prepare_spatial_masks( + regions, max_downscale_factor + ) + self._negative_cross_attn_mask_score = -10000.0 + + def _prepare_spatial_masks( + self, regions: list[TextConditioningRegions], max_downscale_factor: int = 8 + ) -> list[dict[int, torch.Tensor]]: + """Prepare the spatial masks for all downscaling factors.""" + # batch_masks_by_seq_len[b][s] contains the spatial masks for the b'th batch sample with a query sequence length + # of s. + batch_sample_masks_by_seq_len: list[dict[int, torch.Tensor]] = [] + + for batch_sample_regions in regions: + batch_sample_masks_by_seq_len.append({}) + + batch_sample_masks = batch_sample_regions.masks.to(device=self._device, dtype=self._dtype) + + # Downsample the spatial dimensions by factors of 2 until max_downscale_factor is reached. + downscale_factor = 1 + while downscale_factor <= max_downscale_factor: + b, _num_prompts, h, w = batch_sample_masks.shape + assert b == 1 + query_seq_len = h * w + + batch_sample_masks_by_seq_len[-1][query_seq_len] = batch_sample_masks + + downscale_factor *= 2 + if downscale_factor <= max_downscale_factor: + # We use max pooling because we downscale to a pretty low resolution, so we don't want small prompt + # regions to be lost entirely. + # + # ceil_mode=True is set to mirror the downsampling behavior of SD and SDXL. + # + # TODO(ryand): In the future, we may want to experiment with other downsampling methods (e.g. + # nearest interpolation), and could potentially use a weighted mask rather than a binary mask. + batch_sample_masks = F.max_pool2d(batch_sample_masks, kernel_size=2, stride=2, ceil_mode=True) + + return batch_sample_masks_by_seq_len + + def get_cross_attn_mask(self, query_seq_len: int, key_seq_len: int) -> torch.Tensor: + """Get the cross-attention mask for the given query sequence length. + Args: + query_seq_len: The length of the flattened spatial features at the current downscaling level. + key_seq_len (int): The sequence length of the prompt embeddings (which act as the key in the cross-attention + layers). This is most likely equal to the max embedding range end, but we pass it explicitly to be sure. + Returns: + torch.Tensor: The cross-attention score mask. + shape: (batch_size, query_seq_len, key_seq_len). + dtype: float + """ + batch_size = len(self._spatial_masks_by_seq_len) + batch_spatial_masks = [self._spatial_masks_by_seq_len[b][query_seq_len] for b in range(batch_size)] + + # Create an empty attention mask with the correct shape. + attn_mask = torch.zeros((batch_size, query_seq_len, key_seq_len), dtype=self._dtype, device=self._device) + + for batch_idx in range(batch_size): + batch_sample_spatial_masks = batch_spatial_masks[batch_idx] + batch_sample_regions = self._regions[batch_idx] + + # Flatten the spatial dimensions of the mask by reshaping to (1, num_prompts, query_seq_len, 1). + _, num_prompts, _, _ = batch_sample_spatial_masks.shape + batch_sample_query_masks = batch_sample_spatial_masks.view((1, num_prompts, query_seq_len, 1)) + + for prompt_idx, embedding_range in enumerate(batch_sample_regions.ranges): + batch_sample_query_scores = batch_sample_query_masks[0, prompt_idx, :, :].clone() + batch_sample_query_mask = batch_sample_query_scores > 0.5 + batch_sample_query_scores[batch_sample_query_mask] = 0.0 + batch_sample_query_scores[~batch_sample_query_mask] = self._negative_cross_attn_mask_score + attn_mask[batch_idx, :, embedding_range.start : embedding_range.end] = batch_sample_query_scores + + return attn_mask diff --git a/invokeai/backend/stable_diffusion/diffusion/shared_invokeai_diffusion.py b/invokeai/backend/stable_diffusion/diffusion/shared_invokeai_diffusion.py new file mode 100644 index 00000000000..f418133e49f --- /dev/null +++ b/invokeai/backend/stable_diffusion/diffusion/shared_invokeai_diffusion.py @@ -0,0 +1,496 @@ +from __future__ import annotations + +import math +from typing import Any, Callable, Optional, Union + +import torch +from typing_extensions import TypeAlias + +from invokeai.app.services.config.config_default import get_config +from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ( + IPAdapterData, + Range, + TextConditioningData, + TextConditioningRegions, +) +from invokeai.backend.stable_diffusion.diffusion.regional_ip_data import RegionalIPData +from invokeai.backend.stable_diffusion.diffusion.regional_prompt_data import RegionalPromptData + +ModelForwardCallback: TypeAlias = Union[ + # x, t, conditioning, Optional[cross-attention kwargs] + Callable[ + [torch.Tensor, torch.Tensor, torch.Tensor, Optional[dict[str, Any]]], + torch.Tensor, + ], + Callable[[torch.Tensor, torch.Tensor, torch.Tensor], torch.Tensor], +] + + +class InvokeAIDiffuserComponent: + """ + The aim of this component is to provide a single place for code that can be applied identically to + all InvokeAI diffusion procedures. + + At the moment it includes the following features: + * Cross attention control ("prompt2prompt") + * Hybrid conditioning (used for inpainting) + """ + + debug_thresholding = False + sequential_guidance = False + + def __init__( + self, + model, + model_forward_callback: ModelForwardCallback, + ): + """ + :param model: the unet model to pass through to cross attention control + :param model_forward_callback: a lambda with arguments (x, sigma, conditioning_to_apply). will be called repeatedly. most likely, this should simply call model.forward(x, sigma, conditioning) + """ + config = get_config() + self.conditioning = None + self.model = model + self.model_forward_callback = model_forward_callback + self.sequential_guidance = config.sequential_guidance + + def do_controlnet_step( + self, + control_data, + sample: torch.Tensor, + timestep: torch.Tensor, + step_index: int, + total_step_count: int, + conditioning_data: TextConditioningData, + ): + down_block_res_samples, mid_block_res_sample = None, None + + # control_data should be type List[ControlNetData] + # this loop covers both ControlNet (one ControlNetData in list) + # and MultiControlNet (multiple ControlNetData in list) + for _i, control_datum in enumerate(control_data): + control_mode = control_datum.control_mode + # soft_injection and cfg_injection are the two ControlNet control_mode booleans + # that are combined at higher level to make control_mode enum + # soft_injection determines whether to do per-layer re-weighting adjustment (if True) + # or default weighting (if False) + soft_injection = control_mode == "more_prompt" or control_mode == "more_control" + # cfg_injection = determines whether to apply ControlNet to only the conditional (if True) + # or the default both conditional and unconditional (if False) + cfg_injection = control_mode == "more_control" or control_mode == "unbalanced" + + first_control_step = math.floor(control_datum.begin_step_percent * total_step_count) + last_control_step = math.ceil(control_datum.end_step_percent * total_step_count) + # only apply controlnet if current step is within the controlnet's begin/end step range + if step_index >= first_control_step and step_index <= last_control_step: + if cfg_injection: + sample_model_input = sample + else: + # expand the latents input to control model if doing classifier free guidance + # (which I think for now is always true, there is conditional elsewhere that stops execution if + # classifier_free_guidance is <= 1.0 ?) + sample_model_input = torch.cat([sample] * 2) + + added_cond_kwargs = None + + if cfg_injection: # only applying ControlNet to conditional instead of in unconditioned + if conditioning_data.is_sdxl(): + added_cond_kwargs = { + "text_embeds": conditioning_data.cond_text.pooled_embeds, + "time_ids": conditioning_data.cond_text.add_time_ids, + } + encoder_hidden_states = conditioning_data.cond_text.embeds + encoder_attention_mask = None + else: + if conditioning_data.is_sdxl(): + added_cond_kwargs = { + "text_embeds": torch.cat( + [ + # TODO: how to pad? just by zeros? or even truncate? + conditioning_data.uncond_text.pooled_embeds, + conditioning_data.cond_text.pooled_embeds, + ], + dim=0, + ), + "time_ids": torch.cat( + [ + conditioning_data.uncond_text.add_time_ids, + conditioning_data.cond_text.add_time_ids, + ], + dim=0, + ), + } + ( + encoder_hidden_states, + encoder_attention_mask, + ) = self._concat_conditionings_for_batch( + conditioning_data.uncond_text.embeds, + conditioning_data.cond_text.embeds, + ) + if isinstance(control_datum.weight, list): + # if controlnet has multiple weights, use the weight for the current step + controlnet_weight = control_datum.weight[step_index] + else: + # if controlnet has a single weight, use it for all steps + controlnet_weight = control_datum.weight + + # controlnet(s) inference + down_samples, mid_sample = control_datum.model( + sample=sample_model_input, + timestep=timestep, + encoder_hidden_states=encoder_hidden_states, + controlnet_cond=control_datum.image_tensor, + conditioning_scale=controlnet_weight, # controlnet specific, NOT the guidance scale + encoder_attention_mask=encoder_attention_mask, + added_cond_kwargs=added_cond_kwargs, + guess_mode=soft_injection, # this is still called guess_mode in diffusers ControlNetModel + return_dict=False, + ) + if cfg_injection: + # Inferred ControlNet only for the conditional batch. + # To apply the output of ControlNet to both the unconditional and conditional batches, + # prepend zeros for unconditional batch + down_samples = [torch.cat([torch.zeros_like(d), d]) for d in down_samples] + mid_sample = torch.cat([torch.zeros_like(mid_sample), mid_sample]) + + if down_block_res_samples is None and mid_block_res_sample is None: + down_block_res_samples, mid_block_res_sample = down_samples, mid_sample + else: + # add controlnet outputs together if have multiple controlnets + down_block_res_samples = [ + samples_prev + samples_curr + for samples_prev, samples_curr in zip(down_block_res_samples, down_samples, strict=True) + ] + mid_block_res_sample += mid_sample + + return down_block_res_samples, mid_block_res_sample + + def do_unet_step( + self, + sample: torch.Tensor, + timestep: torch.Tensor, + conditioning_data: TextConditioningData, + ip_adapter_data: Optional[list[IPAdapterData]], + step_index: int, + total_step_count: int, + down_block_additional_residuals: Optional[torch.Tensor] = None, # for ControlNet + mid_block_additional_residual: Optional[torch.Tensor] = None, # for ControlNet + down_intrablock_additional_residuals: Optional[torch.Tensor] = None, # for T2I-Adapter + ): + if self.sequential_guidance: + ( + unconditioned_next_x, + conditioned_next_x, + ) = self._apply_standard_conditioning_sequentially( + x=sample, + sigma=timestep, + conditioning_data=conditioning_data, + ip_adapter_data=ip_adapter_data, + step_index=step_index, + total_step_count=total_step_count, + down_block_additional_residuals=down_block_additional_residuals, + mid_block_additional_residual=mid_block_additional_residual, + down_intrablock_additional_residuals=down_intrablock_additional_residuals, + ) + else: + ( + unconditioned_next_x, + conditioned_next_x, + ) = self._apply_standard_conditioning( + x=sample, + sigma=timestep, + conditioning_data=conditioning_data, + ip_adapter_data=ip_adapter_data, + step_index=step_index, + total_step_count=total_step_count, + down_block_additional_residuals=down_block_additional_residuals, + mid_block_additional_residual=mid_block_additional_residual, + down_intrablock_additional_residuals=down_intrablock_additional_residuals, + ) + + return unconditioned_next_x, conditioned_next_x + + def _concat_conditionings_for_batch(self, unconditioning, conditioning): + def _pad_conditioning(cond, target_len, encoder_attention_mask): + conditioning_attention_mask = torch.ones( + (cond.shape[0], cond.shape[1]), device=cond.device, dtype=cond.dtype + ) + + if cond.shape[1] < max_len: + conditioning_attention_mask = torch.cat( + [ + conditioning_attention_mask, + torch.zeros((cond.shape[0], max_len - cond.shape[1]), device=cond.device, dtype=cond.dtype), + ], + dim=1, + ) + + cond = torch.cat( + [ + cond, + torch.zeros( + (cond.shape[0], max_len - cond.shape[1], cond.shape[2]), + device=cond.device, + dtype=cond.dtype, + ), + ], + dim=1, + ) + + if encoder_attention_mask is None: + encoder_attention_mask = conditioning_attention_mask + else: + encoder_attention_mask = torch.cat( + [ + encoder_attention_mask, + conditioning_attention_mask, + ] + ) + + return cond, encoder_attention_mask + + encoder_attention_mask = None + if unconditioning.shape[1] != conditioning.shape[1]: + max_len = max(unconditioning.shape[1], conditioning.shape[1]) + unconditioning, encoder_attention_mask = _pad_conditioning(unconditioning, max_len, encoder_attention_mask) + conditioning, encoder_attention_mask = _pad_conditioning(conditioning, max_len, encoder_attention_mask) + + return torch.cat([unconditioning, conditioning]), encoder_attention_mask + + # methods below are called from do_diffusion_step and should be considered private to this class. + + def _apply_standard_conditioning( + self, + x: torch.Tensor, + sigma: torch.Tensor, + conditioning_data: TextConditioningData, + ip_adapter_data: Optional[list[IPAdapterData]], + step_index: int, + total_step_count: int, + down_block_additional_residuals: Optional[torch.Tensor] = None, # for ControlNet + mid_block_additional_residual: Optional[torch.Tensor] = None, # for ControlNet + down_intrablock_additional_residuals: Optional[torch.Tensor] = None, # for T2I-Adapter + ) -> tuple[torch.Tensor, torch.Tensor]: + """Runs the conditioned and unconditioned UNet forward passes in a single batch for faster inference speed at + the cost of higher memory usage. + """ + x_twice = torch.cat([x] * 2) + sigma_twice = torch.cat([sigma] * 2) + + cross_attention_kwargs = {} + if ip_adapter_data is not None: + ip_adapter_conditioning = [ipa.ip_adapter_conditioning for ipa in ip_adapter_data] + # Note that we 'stack' to produce tensors of shape (batch_size, num_ip_images, seq_len, token_len). + image_prompt_embeds = [ + torch.stack([ipa_conditioning.uncond_image_prompt_embeds, ipa_conditioning.cond_image_prompt_embeds]) + for ipa_conditioning in ip_adapter_conditioning + ] + scales = [ipa.scale_for_step(step_index, total_step_count) for ipa in ip_adapter_data] + ip_masks = [ipa.mask for ipa in ip_adapter_data] + regional_ip_data = RegionalIPData( + image_prompt_embeds=image_prompt_embeds, scales=scales, masks=ip_masks, dtype=x.dtype, device=x.device + ) + cross_attention_kwargs["regional_ip_data"] = regional_ip_data + + added_cond_kwargs = None + if conditioning_data.is_sdxl(): + added_cond_kwargs = { + "text_embeds": torch.cat( + [ + # TODO: how to pad? just by zeros? or even truncate? + conditioning_data.uncond_text.pooled_embeds, + conditioning_data.cond_text.pooled_embeds, + ], + dim=0, + ), + "time_ids": torch.cat( + [ + conditioning_data.uncond_text.add_time_ids, + conditioning_data.cond_text.add_time_ids, + ], + dim=0, + ), + } + + if conditioning_data.cond_regions is not None or conditioning_data.uncond_regions is not None: + # TODO(ryand): We currently initialize RegionalPromptData for every denoising step. The text conditionings + # and masks are not changing from step-to-step, so this really only needs to be done once. While this seems + # painfully inefficient, the time spent is typically negligible compared to the forward inference pass of + # the UNet. The main reason that this hasn't been moved up to eliminate redundancy is that it is slightly + # awkward to handle both standard conditioning and sequential conditioning further up the stack. + regions = [] + for c, r in [ + (conditioning_data.uncond_text, conditioning_data.uncond_regions), + (conditioning_data.cond_text, conditioning_data.cond_regions), + ]: + if r is None: + # Create a dummy mask and range for text conditioning that doesn't have region masks. + _, _, h, w = x.shape + r = TextConditioningRegions( + masks=torch.ones((1, 1, h, w), dtype=x.dtype), + ranges=[Range(start=0, end=c.embeds.shape[1])], + ) + regions.append(r) + + cross_attention_kwargs["regional_prompt_data"] = RegionalPromptData( + regions=regions, device=x.device, dtype=x.dtype + ) + cross_attention_kwargs["percent_through"] = step_index / total_step_count + + both_conditionings, encoder_attention_mask = self._concat_conditionings_for_batch( + conditioning_data.uncond_text.embeds, conditioning_data.cond_text.embeds + ) + both_results = self.model_forward_callback( + x_twice, + sigma_twice, + both_conditionings, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + down_block_additional_residuals=down_block_additional_residuals, + mid_block_additional_residual=mid_block_additional_residual, + down_intrablock_additional_residuals=down_intrablock_additional_residuals, + added_cond_kwargs=added_cond_kwargs, + ) + unconditioned_next_x, conditioned_next_x = both_results.chunk(2) + return unconditioned_next_x, conditioned_next_x + + def _apply_standard_conditioning_sequentially( + self, + x: torch.Tensor, + sigma, + conditioning_data: TextConditioningData, + ip_adapter_data: Optional[list[IPAdapterData]], + step_index: int, + total_step_count: int, + down_block_additional_residuals: Optional[torch.Tensor] = None, # for ControlNet + mid_block_additional_residual: Optional[torch.Tensor] = None, # for ControlNet + down_intrablock_additional_residuals: Optional[torch.Tensor] = None, # for T2I-Adapter + ): + """Runs the conditioned and unconditioned UNet forward passes sequentially for lower memory usage at the cost of + slower execution speed. + """ + # Since we are running the conditioned and unconditioned passes sequentially, we need to split the ControlNet + # and T2I-Adapter residuals into two chunks. + uncond_down_block, cond_down_block = None, None + if down_block_additional_residuals is not None: + uncond_down_block, cond_down_block = [], [] + for down_block in down_block_additional_residuals: + _uncond_down, _cond_down = down_block.chunk(2) + uncond_down_block.append(_uncond_down) + cond_down_block.append(_cond_down) + + uncond_down_intrablock, cond_down_intrablock = None, None + if down_intrablock_additional_residuals is not None: + uncond_down_intrablock, cond_down_intrablock = [], [] + for down_intrablock in down_intrablock_additional_residuals: + _uncond_down, _cond_down = down_intrablock.chunk(2) + uncond_down_intrablock.append(_uncond_down) + cond_down_intrablock.append(_cond_down) + + uncond_mid_block, cond_mid_block = None, None + if mid_block_additional_residual is not None: + uncond_mid_block, cond_mid_block = mid_block_additional_residual.chunk(2) + + ##################### + # Unconditioned pass + ##################### + + cross_attention_kwargs = {} + + # Prepare IP-Adapter cross-attention kwargs for the unconditioned pass. + if ip_adapter_data is not None: + ip_adapter_conditioning = [ipa.ip_adapter_conditioning for ipa in ip_adapter_data] + # Note that we 'unsqueeze' to produce tensors of shape (batch_size=1, num_ip_images, seq_len, token_len). + image_prompt_embeds = [ + torch.unsqueeze(ipa_conditioning.uncond_image_prompt_embeds, dim=0) + for ipa_conditioning in ip_adapter_conditioning + ] + + scales = [ipa.scale_for_step(step_index, total_step_count) for ipa in ip_adapter_data] + ip_masks = [ipa.mask for ipa in ip_adapter_data] + regional_ip_data = RegionalIPData( + image_prompt_embeds=image_prompt_embeds, scales=scales, masks=ip_masks, dtype=x.dtype, device=x.device + ) + cross_attention_kwargs["regional_ip_data"] = regional_ip_data + + # Prepare SDXL conditioning kwargs for the unconditioned pass. + added_cond_kwargs = None + if conditioning_data.is_sdxl(): + added_cond_kwargs = { + "text_embeds": conditioning_data.uncond_text.pooled_embeds, + "time_ids": conditioning_data.uncond_text.add_time_ids, + } + + # Prepare prompt regions for the unconditioned pass. + if conditioning_data.uncond_regions is not None: + cross_attention_kwargs["regional_prompt_data"] = RegionalPromptData( + regions=[conditioning_data.uncond_regions], device=x.device, dtype=x.dtype + ) + cross_attention_kwargs["percent_through"] = step_index / total_step_count + + # Run unconditioned UNet denoising (i.e. negative prompt). + unconditioned_next_x = self.model_forward_callback( + x, + sigma, + conditioning_data.uncond_text.embeds, + cross_attention_kwargs=cross_attention_kwargs, + down_block_additional_residuals=uncond_down_block, + mid_block_additional_residual=uncond_mid_block, + down_intrablock_additional_residuals=uncond_down_intrablock, + added_cond_kwargs=added_cond_kwargs, + ) + + ################### + # Conditioned pass + ################### + + cross_attention_kwargs = {} + + if ip_adapter_data is not None: + ip_adapter_conditioning = [ipa.ip_adapter_conditioning for ipa in ip_adapter_data] + # Note that we 'unsqueeze' to produce tensors of shape (batch_size=1, num_ip_images, seq_len, token_len). + image_prompt_embeds = [ + torch.unsqueeze(ipa_conditioning.cond_image_prompt_embeds, dim=0) + for ipa_conditioning in ip_adapter_conditioning + ] + + scales = [ipa.scale_for_step(step_index, total_step_count) for ipa in ip_adapter_data] + ip_masks = [ipa.mask for ipa in ip_adapter_data] + regional_ip_data = RegionalIPData( + image_prompt_embeds=image_prompt_embeds, scales=scales, masks=ip_masks, dtype=x.dtype, device=x.device + ) + cross_attention_kwargs["regional_ip_data"] = regional_ip_data + + # Prepare SDXL conditioning kwargs for the conditioned pass. + added_cond_kwargs = None + if conditioning_data.is_sdxl(): + added_cond_kwargs = { + "text_embeds": conditioning_data.cond_text.pooled_embeds, + "time_ids": conditioning_data.cond_text.add_time_ids, + } + + # Prepare prompt regions for the conditioned pass. + if conditioning_data.cond_regions is not None: + cross_attention_kwargs["regional_prompt_data"] = RegionalPromptData( + regions=[conditioning_data.cond_regions], device=x.device, dtype=x.dtype + ) + cross_attention_kwargs["percent_through"] = step_index / total_step_count + + # Run conditioned UNet denoising (i.e. positive prompt). + conditioned_next_x = self.model_forward_callback( + x, + sigma, + conditioning_data.cond_text.embeds, + cross_attention_kwargs=cross_attention_kwargs, + down_block_additional_residuals=cond_down_block, + mid_block_additional_residual=cond_mid_block, + down_intrablock_additional_residuals=cond_down_intrablock, + added_cond_kwargs=added_cond_kwargs, + ) + return unconditioned_next_x, conditioned_next_x + + def _combine(self, unconditioned_next_x, conditioned_next_x, guidance_scale): + # to scale how much effect conditioning has, calculate the changes it does and then scale that + scaled_delta = (conditioned_next_x - unconditioned_next_x) * guidance_scale + combined_next_x = unconditioned_next_x + scaled_delta + return combined_next_x diff --git a/invokeai/backend/stable_diffusion/diffusion/unet_attention_patcher.py b/invokeai/backend/stable_diffusion/diffusion/unet_attention_patcher.py new file mode 100644 index 00000000000..00accea6258 --- /dev/null +++ b/invokeai/backend/stable_diffusion/diffusion/unet_attention_patcher.py @@ -0,0 +1,75 @@ +from contextlib import contextmanager +from typing import List, Optional, TypedDict + +from diffusers.models import UNet2DConditionModel + +from invokeai.backend.ip_adapter.ip_adapter import IPAdapter +from invokeai.backend.stable_diffusion.diffusion.custom_atttention import ( + CustomAttnProcessor2_0, + IPAdapterAttentionWeights, +) + + +class UNetIPAdapterData(TypedDict): + ip_adapter: IPAdapter + target_blocks: List[str] # Blocks where IP-Adapter should be applied + method: str # Style or other method type + + +class UNetAttentionPatcher: + """A class for patching a UNet with CustomAttnProcessor2_0 attention layers.""" + + def __init__(self, ip_adapter_data: Optional[List[UNetIPAdapterData]]): + self._ip_adapters = ip_adapter_data + + def _prepare_attention_processors(self, unet: UNet2DConditionModel): + """Prepare a dict of attention processors that can be injected into a unet, and load the IP-Adapter attention + weights into them (if IP-Adapters are being applied). + Note that the `unet` param is only used to determine attention block dimensions and naming. + """ + # Construct a dict of attention processors based on the UNet's architecture. + attn_procs = {} + for idx, name in enumerate(unet.attn_processors.keys()): + if name.endswith("attn1.processor") or self._ip_adapters is None: + # "attn1" processors do not use IP-Adapters. + attn_procs[name] = CustomAttnProcessor2_0() + else: + # Collect the weights from each IP Adapter for the idx'th attention processor. + ip_adapter_attention_weights_collection: list[IPAdapterAttentionWeights] = [] + + for ip_adapter in self._ip_adapters: + ip_adapter_weights = ip_adapter["ip_adapter"].attn_weights.get_attention_processor_weights(idx) + skip = True + negative = False + for block in ip_adapter["target_blocks"]: + if block in name: + skip = False + negative = ip_adapter["method"] == "style_precise" and ( + block == "down_blocks.2.attentions.1" + or block == "down_blocks.2" + or block == "mid_block" + ) + break + ip_adapter_attention_weights: IPAdapterAttentionWeights = IPAdapterAttentionWeights( + ip_adapter_weights=ip_adapter_weights, skip=skip, negative=negative + ) + ip_adapter_attention_weights_collection.append(ip_adapter_attention_weights) + + attn_procs[name] = CustomAttnProcessor2_0(ip_adapter_attention_weights_collection) + + return attn_procs + + @contextmanager + def apply_ip_adapter_attention(self, unet: UNet2DConditionModel): + """A context manager that patches `unet` with CustomAttnProcessor2_0 attention layers.""" + attn_procs = self._prepare_attention_processors(unet) + orig_attn_processors = unet.attn_processors + + try: + # Note to future devs: set_attn_processor(...) does something slightly unexpected - it pops elements from + # the passed dict. So, if you wanted to keep the dict for future use, you'd have to make a + # moderately-shallow copy of it. E.g. `attn_procs_copy = {k: v for k, v in attn_procs.items()}`. + unet.set_attn_processor(attn_procs) + yield None + finally: + unet.set_attn_processor(orig_attn_processors) diff --git a/invokeai/backend/stable_diffusion/diffusion_backend.py b/invokeai/backend/stable_diffusion/diffusion_backend.py new file mode 100644 index 00000000000..4191db734f9 --- /dev/null +++ b/invokeai/backend/stable_diffusion/diffusion_backend.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +import torch +from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel +from diffusers.schedulers.scheduling_utils import SchedulerMixin, SchedulerOutput +from tqdm.auto import tqdm + +from invokeai.app.services.config.config_default import get_config +from invokeai.backend.stable_diffusion.denoise_context import DenoiseContext, UNetKwargs +from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningMode +from invokeai.backend.stable_diffusion.extension_callback_type import ExtensionCallbackType +from invokeai.backend.stable_diffusion.extensions_manager import ExtensionsManager + + +class StableDiffusionBackend: + def __init__( + self, + unet: UNet2DConditionModel, + scheduler: SchedulerMixin, + ): + self.unet = unet + self.scheduler = scheduler + config = get_config() + self._sequential_guidance = config.sequential_guidance + + def latents_from_embeddings(self, ctx: DenoiseContext, ext_manager: ExtensionsManager): + if ctx.inputs.init_timestep.shape[0] == 0: + return ctx.inputs.orig_latents + + ctx.latents = ctx.inputs.orig_latents.clone() + + if ctx.inputs.noise is not None: + batch_size = ctx.latents.shape[0] + # latents = noise * self.scheduler.init_noise_sigma # it's like in t2l according to diffusers + ctx.latents = ctx.scheduler.add_noise( + ctx.latents, ctx.inputs.noise, ctx.inputs.init_timestep.expand(batch_size) + ) + + # if no work to do, return latents + if ctx.inputs.timesteps.shape[0] == 0: + return ctx.latents + + # ext: inpaint[pre_denoise_loop, priority=normal] (maybe init, but not sure if it needed) + # ext: preview[pre_denoise_loop, priority=low] + ext_manager.run_callback(ExtensionCallbackType.PRE_DENOISE_LOOP, ctx) + + for ctx.step_index, ctx.timestep in enumerate(tqdm(ctx.inputs.timesteps)): # noqa: B020 + # ext: inpaint (apply mask to latents on non-inpaint models) + ext_manager.run_callback(ExtensionCallbackType.PRE_STEP, ctx) + + # ext: tiles? [override: step] + ctx.step_output = self.step(ctx, ext_manager) + + # ext: inpaint[post_step, priority=high] (apply mask to preview on non-inpaint models) + # ext: preview[post_step, priority=low] + ext_manager.run_callback(ExtensionCallbackType.POST_STEP, ctx) + + ctx.latents = ctx.step_output.prev_sample + + # ext: inpaint[post_denoise_loop] (restore unmasked part) + ext_manager.run_callback(ExtensionCallbackType.POST_DENOISE_LOOP, ctx) + return ctx.latents + + @torch.inference_mode() + def step(self, ctx: DenoiseContext, ext_manager: ExtensionsManager) -> SchedulerOutput: + ctx.latent_model_input = ctx.scheduler.scale_model_input(ctx.latents, ctx.timestep) + + # TODO: conditionings as list(conditioning_data.to_unet_kwargs - ready) + # Note: The current handling of conditioning doesn't feel very future-proof. + # This might change in the future as new requirements come up, but for now, + # this is the rough plan. + if self._sequential_guidance: + ctx.negative_noise_pred = self.run_unet(ctx, ext_manager, ConditioningMode.Negative) + ctx.positive_noise_pred = self.run_unet(ctx, ext_manager, ConditioningMode.Positive) + else: + both_noise_pred = self.run_unet(ctx, ext_manager, ConditioningMode.Both) + ctx.negative_noise_pred, ctx.positive_noise_pred = both_noise_pred.chunk(2) + + # ext: override combine_noise_preds + ctx.noise_pred = self.combine_noise_preds(ctx) + + # ext: cfg_rescale [modify_noise_prediction] + # TODO: rename + ext_manager.run_callback(ExtensionCallbackType.POST_COMBINE_NOISE_PREDS, ctx) + + # compute the previous noisy sample x_t -> x_t-1 + step_output = ctx.scheduler.step(ctx.noise_pred, ctx.timestep, ctx.latents, **ctx.inputs.scheduler_step_kwargs) + + # clean up locals + ctx.latent_model_input = None + ctx.negative_noise_pred = None + ctx.positive_noise_pred = None + ctx.noise_pred = None + + return step_output + + @staticmethod + def combine_noise_preds(ctx: DenoiseContext) -> torch.Tensor: + guidance_scale = ctx.inputs.conditioning_data.guidance_scale + if isinstance(guidance_scale, list): + guidance_scale = guidance_scale[ctx.step_index] + + # Note: Although this `torch.lerp(...)` line is logically equivalent to the current CFG line, it seems to result + # in slightly different outputs. It is suspected that this is caused by small precision differences. + # return torch.lerp(ctx.negative_noise_pred, ctx.positive_noise_pred, guidance_scale) + return ctx.negative_noise_pred + guidance_scale * (ctx.positive_noise_pred - ctx.negative_noise_pred) + + def run_unet(self, ctx: DenoiseContext, ext_manager: ExtensionsManager, conditioning_mode: ConditioningMode): + sample = ctx.latent_model_input + if conditioning_mode == ConditioningMode.Both: + sample = torch.cat([sample] * 2) + + ctx.unet_kwargs = UNetKwargs( + sample=sample, + timestep=ctx.timestep, + encoder_hidden_states=None, # set later by conditoning + cross_attention_kwargs=dict( # noqa: C408 + percent_through=ctx.step_index / len(ctx.inputs.timesteps), + ), + ) + + ctx.conditioning_mode = conditioning_mode + ctx.inputs.conditioning_data.to_unet_kwargs(ctx.unet_kwargs, ctx.conditioning_mode) + + # ext: controlnet/ip/t2i [pre_unet] + ext_manager.run_callback(ExtensionCallbackType.PRE_UNET, ctx) + + # ext: inpaint [pre_unet, priority=low] + # or + # ext: inpaint [override: unet_forward] + noise_pred = self._unet_forward(**vars(ctx.unet_kwargs)) + + ext_manager.run_callback(ExtensionCallbackType.POST_UNET, ctx) + + # clean up locals + ctx.unet_kwargs = None + ctx.conditioning_mode = None + + return noise_pred + + def _unet_forward(self, **kwargs) -> torch.Tensor: + return self.unet(**kwargs).sample diff --git a/invokeai/backend/stable_diffusion/extension_callback_type.py b/invokeai/backend/stable_diffusion/extension_callback_type.py new file mode 100644 index 00000000000..e4c365007ba --- /dev/null +++ b/invokeai/backend/stable_diffusion/extension_callback_type.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class ExtensionCallbackType(Enum): + SETUP = "setup" + PRE_DENOISE_LOOP = "pre_denoise_loop" + POST_DENOISE_LOOP = "post_denoise_loop" + PRE_STEP = "pre_step" + POST_STEP = "post_step" + PRE_UNET = "pre_unet" + POST_UNET = "post_unet" + POST_COMBINE_NOISE_PREDS = "post_combine_noise_preds" diff --git a/invokeai/backend/stable_diffusion/extensions/base.py b/invokeai/backend/stable_diffusion/extensions/base.py new file mode 100644 index 00000000000..a3d27464a0c --- /dev/null +++ b/invokeai/backend/stable_diffusion/extensions/base.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from contextlib import contextmanager +from dataclasses import dataclass +from typing import TYPE_CHECKING, Callable, Dict, List + +from diffusers import UNet2DConditionModel + +if TYPE_CHECKING: + from invokeai.backend.stable_diffusion.denoise_context import DenoiseContext + from invokeai.backend.stable_diffusion.extension_callback_type import ExtensionCallbackType + from invokeai.backend.util.original_weights_storage import OriginalWeightsStorage + + +@dataclass +class CallbackMetadata: + callback_type: ExtensionCallbackType + order: int + + +@dataclass +class CallbackFunctionWithMetadata: + metadata: CallbackMetadata + function: Callable[[DenoiseContext], None] + + +def callback(callback_type: ExtensionCallbackType, order: int = 0): + def _decorator(function): + function._ext_metadata = CallbackMetadata( + callback_type=callback_type, + order=order, + ) + return function + + return _decorator + + +class ExtensionBase: + def __init__(self): + self._callbacks: Dict[ExtensionCallbackType, List[CallbackFunctionWithMetadata]] = {} + + # Register all of the callback methods for this instance. + for func_name in dir(self): + func = getattr(self, func_name) + metadata = getattr(func, "_ext_metadata", None) + if metadata is not None and isinstance(metadata, CallbackMetadata): + if metadata.callback_type not in self._callbacks: + self._callbacks[metadata.callback_type] = [] + self._callbacks[metadata.callback_type].append(CallbackFunctionWithMetadata(metadata, func)) + + def get_callbacks(self): + return self._callbacks + + @contextmanager + def patch_extension(self, ctx: DenoiseContext): + yield None + + @contextmanager + def patch_unet(self, unet: UNet2DConditionModel, original_weights: OriginalWeightsStorage): + """A context manager for applying patches to the UNet model. The context manager's lifetime spans the entire + diffusion process. Weight unpatching is handled upstream, and is achieved by saving unchanged weights by + `original_weights.save` function. Note that this enables some performance optimization by avoiding redundant + operations. All other patches (e.g. changes to tensor shapes, function monkey-patches, etc.) should be unpatched + by this context manager. + + Args: + unet (UNet2DConditionModel): The UNet model on execution device to patch. + original_weights (OriginalWeightsStorage): A storage with copy of the model's original weights in CPU, for + unpatching purposes. Extension should save tensor which being modified in this storage, also extensions + can access original weights values. + """ + yield diff --git a/invokeai/backend/stable_diffusion/extensions/controlnet.py b/invokeai/backend/stable_diffusion/extensions/controlnet.py new file mode 100644 index 00000000000..a48a681af3f --- /dev/null +++ b/invokeai/backend/stable_diffusion/extensions/controlnet.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +import math +from contextlib import contextmanager +from typing import TYPE_CHECKING, List, Optional, Union + +import torch +from PIL.Image import Image + +from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR +from invokeai.app.util.controlnet_utils import CONTROLNET_MODE_VALUES, CONTROLNET_RESIZE_VALUES, prepare_control_image +from invokeai.backend.stable_diffusion.denoise_context import UNetKwargs +from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningMode +from invokeai.backend.stable_diffusion.extension_callback_type import ExtensionCallbackType +from invokeai.backend.stable_diffusion.extensions.base import ExtensionBase, callback + +if TYPE_CHECKING: + from invokeai.backend.stable_diffusion.denoise_context import DenoiseContext + from invokeai.backend.util.hotfixes import ControlNetModel + + +class ControlNetExt(ExtensionBase): + def __init__( + self, + model: ControlNetModel, + image: Image, + weight: Union[float, List[float]], + begin_step_percent: float, + end_step_percent: float, + control_mode: CONTROLNET_MODE_VALUES, + resize_mode: CONTROLNET_RESIZE_VALUES, + ): + super().__init__() + self._model = model + self._image = image + self._weight = weight + self._begin_step_percent = begin_step_percent + self._end_step_percent = end_step_percent + self._control_mode = control_mode + self._resize_mode = resize_mode + + self._image_tensor: Optional[torch.Tensor] = None + + @contextmanager + def patch_extension(self, ctx: DenoiseContext): + original_processors = self._model.attn_processors + try: + self._model.set_attn_processor(ctx.inputs.attention_processor_cls()) + + yield None + finally: + self._model.set_attn_processor(original_processors) + + @callback(ExtensionCallbackType.PRE_DENOISE_LOOP) + def resize_image(self, ctx: DenoiseContext): + _, _, latent_height, latent_width = ctx.latents.shape + image_height = latent_height * LATENT_SCALE_FACTOR + image_width = latent_width * LATENT_SCALE_FACTOR + + self._image_tensor = prepare_control_image( + image=self._image, + do_classifier_free_guidance=False, + width=image_width, + height=image_height, + device=ctx.latents.device, + dtype=ctx.latents.dtype, + control_mode=self._control_mode, + resize_mode=self._resize_mode, + ) + + @callback(ExtensionCallbackType.PRE_UNET) + def pre_unet_step(self, ctx: DenoiseContext): + # skip if model not active in current step + total_steps = len(ctx.inputs.timesteps) + first_step = math.floor(self._begin_step_percent * total_steps) + last_step = math.ceil(self._end_step_percent * total_steps) + if ctx.step_index < first_step or ctx.step_index > last_step: + return + + # convert mode to internal flags + soft_injection = self._control_mode in ["more_prompt", "more_control"] + cfg_injection = self._control_mode in ["more_control", "unbalanced"] + + # no negative conditioning in cfg_injection mode + if cfg_injection: + if ctx.conditioning_mode == ConditioningMode.Negative: + return + down_samples, mid_sample = self._run(ctx, soft_injection, ConditioningMode.Positive) + + if ctx.conditioning_mode == ConditioningMode.Both: + # add zeros as samples for negative conditioning + down_samples = [torch.cat([torch.zeros_like(d), d]) for d in down_samples] + mid_sample = torch.cat([torch.zeros_like(mid_sample), mid_sample]) + + else: + down_samples, mid_sample = self._run(ctx, soft_injection, ctx.conditioning_mode) + + if ( + ctx.unet_kwargs.down_block_additional_residuals is None + and ctx.unet_kwargs.mid_block_additional_residual is None + ): + ctx.unet_kwargs.down_block_additional_residuals = down_samples + ctx.unet_kwargs.mid_block_additional_residual = mid_sample + else: + # add controlnet outputs together if have multiple controlnets + ctx.unet_kwargs.down_block_additional_residuals = [ + samples_prev + samples_curr + for samples_prev, samples_curr in zip( + ctx.unet_kwargs.down_block_additional_residuals, down_samples, strict=True + ) + ] + ctx.unet_kwargs.mid_block_additional_residual += mid_sample + + def _run(self, ctx: DenoiseContext, soft_injection: bool, conditioning_mode: ConditioningMode): + total_steps = len(ctx.inputs.timesteps) + + model_input = ctx.latent_model_input + image_tensor = self._image_tensor + if conditioning_mode == ConditioningMode.Both: + model_input = torch.cat([model_input] * 2) + image_tensor = torch.cat([image_tensor] * 2) + + cn_unet_kwargs = UNetKwargs( + sample=model_input, + timestep=ctx.timestep, + encoder_hidden_states=None, # set later by conditioning + cross_attention_kwargs=dict( # noqa: C408 + percent_through=ctx.step_index / total_steps, + ), + ) + + ctx.inputs.conditioning_data.to_unet_kwargs(cn_unet_kwargs, conditioning_mode=conditioning_mode) + + # get static weight, or weight corresponding to current step + weight = self._weight + if isinstance(weight, list): + weight = weight[ctx.step_index] + + tmp_kwargs = vars(cn_unet_kwargs) + + # Remove kwargs not related to ControlNet unet + # ControlNet guidance fields + del tmp_kwargs["down_block_additional_residuals"] + del tmp_kwargs["mid_block_additional_residual"] + + # T2i Adapter guidance fields + del tmp_kwargs["down_intrablock_additional_residuals"] + + # controlnet(s) inference + down_samples, mid_sample = self._model( + controlnet_cond=image_tensor, + conditioning_scale=weight, # controlnet specific, NOT the guidance scale + guess_mode=soft_injection, # this is still called guess_mode in diffusers ControlNetModel + return_dict=False, + **vars(cn_unet_kwargs), + ) + + return down_samples, mid_sample diff --git a/invokeai/backend/stable_diffusion/extensions/freeu.py b/invokeai/backend/stable_diffusion/extensions/freeu.py new file mode 100644 index 00000000000..ff54e1a52f6 --- /dev/null +++ b/invokeai/backend/stable_diffusion/extensions/freeu.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from contextlib import contextmanager +from typing import TYPE_CHECKING + +from diffusers import UNet2DConditionModel + +from invokeai.backend.stable_diffusion.extensions.base import ExtensionBase + +if TYPE_CHECKING: + from invokeai.app.shared.models import FreeUConfig + from invokeai.backend.util.original_weights_storage import OriginalWeightsStorage + + +class FreeUExt(ExtensionBase): + def __init__( + self, + freeu_config: FreeUConfig, + ): + super().__init__() + self._freeu_config = freeu_config + + @contextmanager + def patch_unet(self, unet: UNet2DConditionModel, original_weights: OriginalWeightsStorage): + unet.enable_freeu( + b1=self._freeu_config.b1, + b2=self._freeu_config.b2, + s1=self._freeu_config.s1, + s2=self._freeu_config.s2, + ) + + try: + yield + finally: + unet.disable_freeu() diff --git a/invokeai/backend/stable_diffusion/extensions/inpaint.py b/invokeai/backend/stable_diffusion/extensions/inpaint.py new file mode 100644 index 00000000000..00793591558 --- /dev/null +++ b/invokeai/backend/stable_diffusion/extensions/inpaint.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +import einops +import torch +from diffusers import UNet2DConditionModel + +from invokeai.backend.stable_diffusion.extension_callback_type import ExtensionCallbackType +from invokeai.backend.stable_diffusion.extensions.base import ExtensionBase, callback + +if TYPE_CHECKING: + from invokeai.backend.stable_diffusion.denoise_context import DenoiseContext + + +class InpaintExt(ExtensionBase): + """An extension for inpainting with non-inpainting models. See `InpaintModelExt` for inpainting with inpainting + models. + """ + + def __init__( + self, + mask: torch.Tensor, + is_gradient_mask: bool, + ): + """Initialize InpaintExt. + Args: + mask (torch.Tensor): The inpainting mask. Shape: (1, 1, latent_height, latent_width). Values are + expected to be in the range [0, 1]. A value of 1 means that the corresponding 'pixel' should not be + inpainted. + is_gradient_mask (bool): If True, mask is interpreted as a gradient mask meaning that the mask values range + from 0 to 1. If False, mask is interpreted as binary mask meaning that the mask values are either 0 or + 1. + """ + super().__init__() + self._mask = mask + self._is_gradient_mask = is_gradient_mask + + # Noise, which used to noisify unmasked part of image + # if noise provided to context, then it will be used + # if no noise provided, then noise will be generated based on seed + self._noise: Optional[torch.Tensor] = None + + @staticmethod + def _is_normal_model(unet: UNet2DConditionModel): + """Checks if the provided UNet belongs to a regular model. + The `in_channels` of a UNet vary depending on model type: + - normal - 4 + - depth - 5 + - inpaint - 9 + """ + return unet.conv_in.in_channels == 4 + + def _apply_mask(self, ctx: DenoiseContext, latents: torch.Tensor, t: torch.Tensor) -> torch.Tensor: + batch_size = latents.size(0) + mask = einops.repeat(self._mask, "b c h w -> (repeat b) c h w", repeat=batch_size) + if t.dim() == 0: + # some schedulers expect t to be one-dimensional. + # TODO: file diffusers bug about inconsistency? + t = einops.repeat(t, "-> batch", batch=batch_size) + # Noise shouldn't be re-randomized between steps here. The multistep schedulers + # get very confused about what is happening from step to step when we do that. + mask_latents = ctx.scheduler.add_noise(ctx.inputs.orig_latents, self._noise, t) + # TODO: Do we need to also apply scheduler.scale_model_input? Or is add_noise appropriately scaled already? + # mask_latents = self.scheduler.scale_model_input(mask_latents, t) + mask_latents = einops.repeat(mask_latents, "b c h w -> (repeat b) c h w", repeat=batch_size) + if self._is_gradient_mask: + threshold = (t.item()) / ctx.scheduler.config.num_train_timesteps + mask_bool = mask < 1 - threshold + masked_input = torch.where(mask_bool, latents, mask_latents) + else: + masked_input = torch.lerp(latents, mask_latents.to(dtype=latents.dtype), mask.to(dtype=latents.dtype)) + return masked_input + + @callback(ExtensionCallbackType.PRE_DENOISE_LOOP) + def init_tensors(self, ctx: DenoiseContext): + if not self._is_normal_model(ctx.unet): + raise ValueError( + "InpaintExt should be used only on normal (non-inpainting) models. This could be caused by an " + "inpainting model that was incorrectly marked as a non-inpainting model. In some cases, this can be " + "fixed by removing and re-adding the model (so that it gets re-probed)." + ) + + self._mask = self._mask.to(device=ctx.latents.device, dtype=ctx.latents.dtype) + + self._noise = ctx.inputs.noise + # 'noise' might be None if the latents have already been noised (e.g. when running the SDXL refiner). + # We still need noise for inpainting, so we generate it from the seed here. + if self._noise is None: + self._noise = torch.randn( + ctx.latents.shape, + dtype=torch.float32, + device="cpu", + generator=torch.Generator(device="cpu").manual_seed(ctx.seed), + ).to(device=ctx.latents.device, dtype=ctx.latents.dtype) + + # Use negative order to make extensions with default order work with patched latents + @callback(ExtensionCallbackType.PRE_STEP, order=-100) + def apply_mask_to_initial_latents(self, ctx: DenoiseContext): + ctx.latents = self._apply_mask(ctx, ctx.latents, ctx.timestep) + + # TODO: redo this with preview events rewrite + # Use negative order to make extensions with default order work with patched latents + @callback(ExtensionCallbackType.POST_STEP, order=-100) + def apply_mask_to_step_output(self, ctx: DenoiseContext): + timestep = ctx.scheduler.timesteps[-1] + if hasattr(ctx.step_output, "denoised"): + ctx.step_output.denoised = self._apply_mask(ctx, ctx.step_output.denoised, timestep) + elif hasattr(ctx.step_output, "pred_original_sample"): + ctx.step_output.pred_original_sample = self._apply_mask(ctx, ctx.step_output.pred_original_sample, timestep) + else: + ctx.step_output.pred_original_sample = self._apply_mask(ctx, ctx.step_output.prev_sample, timestep) + + # Restore unmasked part after the last step is completed + @callback(ExtensionCallbackType.POST_DENOISE_LOOP) + def restore_unmasked(self, ctx: DenoiseContext): + if self._is_gradient_mask: + ctx.latents = torch.where(self._mask < 1, ctx.latents, ctx.inputs.orig_latents) + else: + ctx.latents = torch.lerp(ctx.latents, ctx.inputs.orig_latents, self._mask) diff --git a/invokeai/backend/stable_diffusion/extensions/inpaint_model.py b/invokeai/backend/stable_diffusion/extensions/inpaint_model.py new file mode 100644 index 00000000000..6ee8ef6311c --- /dev/null +++ b/invokeai/backend/stable_diffusion/extensions/inpaint_model.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +import torch +from diffusers import UNet2DConditionModel + +from invokeai.backend.stable_diffusion.extension_callback_type import ExtensionCallbackType +from invokeai.backend.stable_diffusion.extensions.base import ExtensionBase, callback + +if TYPE_CHECKING: + from invokeai.backend.stable_diffusion.denoise_context import DenoiseContext + + +class InpaintModelExt(ExtensionBase): + """An extension for inpainting with inpainting models. See `InpaintExt` for inpainting with non-inpainting + models. + """ + + def __init__( + self, + mask: Optional[torch.Tensor], + masked_latents: Optional[torch.Tensor], + is_gradient_mask: bool, + ): + """Initialize InpaintModelExt. + Args: + mask (Optional[torch.Tensor]): The inpainting mask. Shape: (1, 1, latent_height, latent_width). Values are + expected to be in the range [0, 1]. A value of 1 means that the corresponding 'pixel' should not be + inpainted. + masked_latents (Optional[torch.Tensor]): Latents of initial image, with masked out by black color inpainted area. + If mask provided, then too should be provided. Shape: (1, 1, latent_height, latent_width) + is_gradient_mask (bool): If True, mask is interpreted as a gradient mask meaning that the mask values range + from 0 to 1. If False, mask is interpreted as binary mask meaning that the mask values are either 0 or + 1. + """ + super().__init__() + if mask is not None and masked_latents is None: + raise ValueError("Source image required for inpaint mask when inpaint model used!") + + # Inverse mask, because inpaint models treat mask as: 0 - remain same, 1 - inpaint + self._mask = None + if mask is not None: + self._mask = 1 - mask + self._masked_latents = masked_latents + self._is_gradient_mask = is_gradient_mask + + @staticmethod + def _is_inpaint_model(unet: UNet2DConditionModel): + """Checks if the provided UNet belongs to a regular model. + The `in_channels` of a UNet vary depending on model type: + - normal - 4 + - depth - 5 + - inpaint - 9 + """ + return unet.conv_in.in_channels == 9 + + @callback(ExtensionCallbackType.PRE_DENOISE_LOOP) + def init_tensors(self, ctx: DenoiseContext): + if not self._is_inpaint_model(ctx.unet): + raise ValueError("InpaintModelExt should be used only on inpaint models!") + + if self._mask is None: + self._mask = torch.ones_like(ctx.latents[:1, :1]) + self._mask = self._mask.to(device=ctx.latents.device, dtype=ctx.latents.dtype) + + if self._masked_latents is None: + self._masked_latents = torch.zeros_like(ctx.latents[:1]) + self._masked_latents = self._masked_latents.to(device=ctx.latents.device, dtype=ctx.latents.dtype) + + # Do last so that other extensions works with normal latents + @callback(ExtensionCallbackType.PRE_UNET, order=1000) + def append_inpaint_layers(self, ctx: DenoiseContext): + batch_size = ctx.unet_kwargs.sample.shape[0] + b_mask = torch.cat([self._mask] * batch_size) + b_masked_latents = torch.cat([self._masked_latents] * batch_size) + ctx.unet_kwargs.sample = torch.cat( + [ctx.unet_kwargs.sample, b_mask, b_masked_latents], + dim=1, + ) + + # Restore unmasked part as inpaint model can change unmasked part slightly + @callback(ExtensionCallbackType.POST_DENOISE_LOOP) + def restore_unmasked(self, ctx: DenoiseContext): + if self._is_gradient_mask: + ctx.latents = torch.where(self._mask > 0, ctx.latents, ctx.inputs.orig_latents) + else: + ctx.latents = torch.lerp(ctx.inputs.orig_latents, ctx.latents, self._mask) diff --git a/invokeai/backend/stable_diffusion/extensions/lora.py b/invokeai/backend/stable_diffusion/extensions/lora.py new file mode 100644 index 00000000000..43986fad4d6 --- /dev/null +++ b/invokeai/backend/stable_diffusion/extensions/lora.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from contextlib import contextmanager +from typing import TYPE_CHECKING + +from diffusers import UNet2DConditionModel + +from invokeai.backend.patches.layer_patcher import LayerPatcher +from invokeai.backend.patches.model_patch_raw import ModelPatchRaw +from invokeai.backend.stable_diffusion.extensions.base import ExtensionBase + +if TYPE_CHECKING: + from invokeai.app.invocations.model import ModelIdentifierField + from invokeai.app.services.shared.invocation_context import InvocationContext + from invokeai.backend.util.original_weights_storage import OriginalWeightsStorage + + +class LoRAExt(ExtensionBase): + def __init__( + self, + node_context: InvocationContext, + model_id: ModelIdentifierField, + weight: float, + ): + super().__init__() + self._node_context = node_context + self._model_id = model_id + self._weight = weight + + @contextmanager + def patch_unet(self, unet: UNet2DConditionModel, original_weights: OriginalWeightsStorage): + lora_model = self._node_context.models.load(self._model_id).model + assert isinstance(lora_model, ModelPatchRaw) + LayerPatcher.apply_smart_model_patch( + model=unet, + prefix="lora_unet_", + patch=lora_model, + patch_weight=self._weight, + original_weights=original_weights, + original_modules={}, + dtype=unet.dtype, + force_direct_patching=True, + force_sidecar_patching=False, + ) + del lora_model + + yield diff --git a/invokeai/backend/stable_diffusion/extensions/preview.py b/invokeai/backend/stable_diffusion/extensions/preview.py new file mode 100644 index 00000000000..6256f475945 --- /dev/null +++ b/invokeai/backend/stable_diffusion/extensions/preview.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Callable, Optional + +import torch + +from invokeai.backend.stable_diffusion.extension_callback_type import ExtensionCallbackType +from invokeai.backend.stable_diffusion.extensions.base import ExtensionBase, callback + +if TYPE_CHECKING: + from invokeai.backend.stable_diffusion.denoise_context import DenoiseContext + + +# TODO: change event to accept image instead of latents +@dataclass +class PipelineIntermediateState: + step: int + order: int + total_steps: int + timestep: int + latents: torch.Tensor + predicted_original: Optional[torch.Tensor] = None + + +class PreviewExt(ExtensionBase): + def __init__(self, callback: Callable[[PipelineIntermediateState], None]): + super().__init__() + self.callback = callback + + # do last so that all other changes shown + @callback(ExtensionCallbackType.PRE_DENOISE_LOOP, order=1000) + def initial_preview(self, ctx: DenoiseContext): + self.callback( + PipelineIntermediateState( + step=0, + order=ctx.scheduler.order, + total_steps=len(ctx.inputs.timesteps), + timestep=int(ctx.scheduler.config.num_train_timesteps), # TODO: is there any code which uses it? + latents=ctx.latents, + ) + ) + + # do last so that all other changes shown + @callback(ExtensionCallbackType.POST_STEP, order=1000) + def step_preview(self, ctx: DenoiseContext): + if hasattr(ctx.step_output, "denoised"): + predicted_original = ctx.step_output.denoised + elif hasattr(ctx.step_output, "pred_original_sample"): + predicted_original = ctx.step_output.pred_original_sample + else: + predicted_original = ctx.step_output.prev_sample + + self.callback( + PipelineIntermediateState( + step=ctx.step_index, + order=ctx.scheduler.order, + total_steps=len(ctx.inputs.timesteps), + timestep=int(ctx.timestep), # TODO: is there any code which uses it? + latents=ctx.step_output.prev_sample, + predicted_original=predicted_original, # TODO: is there any reason for additional field? + ) + ) diff --git a/invokeai/backend/stable_diffusion/extensions/rescale_cfg.py b/invokeai/backend/stable_diffusion/extensions/rescale_cfg.py new file mode 100644 index 00000000000..7cccbb8a2bc --- /dev/null +++ b/invokeai/backend/stable_diffusion/extensions/rescale_cfg.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch + +from invokeai.backend.stable_diffusion.extension_callback_type import ExtensionCallbackType +from invokeai.backend.stable_diffusion.extensions.base import ExtensionBase, callback + +if TYPE_CHECKING: + from invokeai.backend.stable_diffusion.denoise_context import DenoiseContext + + +class RescaleCFGExt(ExtensionBase): + def __init__(self, rescale_multiplier: float): + super().__init__() + self._rescale_multiplier = rescale_multiplier + + @staticmethod + def _rescale_cfg(total_noise_pred: torch.Tensor, pos_noise_pred: torch.Tensor, multiplier: float = 0.7): + """Implementation of Algorithm 2 from https://arxiv.org/pdf/2305.08891.pdf.""" + ro_pos = torch.std(pos_noise_pred, dim=(1, 2, 3), keepdim=True) + ro_cfg = torch.std(total_noise_pred, dim=(1, 2, 3), keepdim=True) + + x_rescaled = total_noise_pred * (ro_pos / ro_cfg) + x_final = multiplier * x_rescaled + (1.0 - multiplier) * total_noise_pred + return x_final + + @callback(ExtensionCallbackType.POST_COMBINE_NOISE_PREDS) + def rescale_noise_pred(self, ctx: DenoiseContext): + if self._rescale_multiplier > 0: + ctx.noise_pred = self._rescale_cfg( + ctx.noise_pred, + ctx.positive_noise_pred, + self._rescale_multiplier, + ) diff --git a/invokeai/backend/stable_diffusion/extensions/seamless.py b/invokeai/backend/stable_diffusion/extensions/seamless.py new file mode 100644 index 00000000000..a96ea6e4d2e --- /dev/null +++ b/invokeai/backend/stable_diffusion/extensions/seamless.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from contextlib import contextmanager +from typing import Callable, Dict, List, Optional, Tuple + +import torch +import torch.nn as nn +from diffusers import UNet2DConditionModel +from diffusers.models.lora import LoRACompatibleConv + +from invokeai.backend.stable_diffusion.extensions.base import ExtensionBase + + +class SeamlessExt(ExtensionBase): + def __init__( + self, + seamless_axes: List[str], + ): + super().__init__() + self._seamless_axes = seamless_axes + + @contextmanager + def patch_unet(self, unet: UNet2DConditionModel, cached_weights: Optional[Dict[str, torch.Tensor]] = None): + with self.static_patch_model( + model=unet, + seamless_axes=self._seamless_axes, + ): + yield + + @staticmethod + @contextmanager + def static_patch_model( + model: torch.nn.Module, + seamless_axes: List[str], + ): + if not seamless_axes: + yield + return + + x_mode = "circular" if "x" in seamless_axes else "constant" + y_mode = "circular" if "y" in seamless_axes else "constant" + + # override conv_forward + # https://github.com/huggingface/diffusers/issues/556#issuecomment-1993287019 + def _conv_forward_asymmetric( + self, input: torch.Tensor, weight: torch.Tensor, bias: Optional[torch.Tensor] = None + ): + self.paddingX = (self._reversed_padding_repeated_twice[0], self._reversed_padding_repeated_twice[1], 0, 0) + self.paddingY = (0, 0, self._reversed_padding_repeated_twice[2], self._reversed_padding_repeated_twice[3]) + working = torch.nn.functional.pad(input, self.paddingX, mode=x_mode) + working = torch.nn.functional.pad(working, self.paddingY, mode=y_mode) + return torch.nn.functional.conv2d( + working, weight, bias, self.stride, torch.nn.modules.utils._pair(0), self.dilation, self.groups + ) + + original_layers: List[Tuple[nn.Conv2d, Callable]] = [] + try: + for layer in model.modules(): + if not isinstance(layer, torch.nn.Conv2d): + continue + + if isinstance(layer, LoRACompatibleConv) and layer.lora_layer is None: + layer.lora_layer = lambda *x: 0 + original_layers.append((layer, layer._conv_forward)) + layer._conv_forward = _conv_forward_asymmetric.__get__(layer, torch.nn.Conv2d) + + yield + + finally: + for layer, orig_conv_forward in original_layers: + layer._conv_forward = orig_conv_forward diff --git a/invokeai/backend/stable_diffusion/extensions/t2i_adapter.py b/invokeai/backend/stable_diffusion/extensions/t2i_adapter.py new file mode 100644 index 00000000000..67fede93664 --- /dev/null +++ b/invokeai/backend/stable_diffusion/extensions/t2i_adapter.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +import math +from typing import TYPE_CHECKING, List, Optional, Union + +import torch +from diffusers import T2IAdapter +from PIL.Image import Image + +from invokeai.app.util.controlnet_utils import prepare_control_image +from invokeai.backend.model_manager.taxonomy import BaseModelType +from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningMode +from invokeai.backend.stable_diffusion.extension_callback_type import ExtensionCallbackType +from invokeai.backend.stable_diffusion.extensions.base import ExtensionBase, callback +from invokeai.backend.util.devices import TorchDevice + +if TYPE_CHECKING: + from invokeai.app.invocations.model import ModelIdentifierField + from invokeai.app.services.shared.invocation_context import InvocationContext + from invokeai.app.util.controlnet_utils import CONTROLNET_RESIZE_VALUES + from invokeai.backend.stable_diffusion.denoise_context import DenoiseContext + + +class T2IAdapterExt(ExtensionBase): + def __init__( + self, + node_context: InvocationContext, + model_id: ModelIdentifierField, + image: Image, + weight: Union[float, List[float]], + begin_step_percent: float, + end_step_percent: float, + resize_mode: CONTROLNET_RESIZE_VALUES, + ): + super().__init__() + self._node_context = node_context + self._model_id = model_id + self._image = image + self._weight = weight + self._resize_mode = resize_mode + self._begin_step_percent = begin_step_percent + self._end_step_percent = end_step_percent + + self._adapter_state: Optional[List[torch.Tensor]] = None + + # The max_unet_downscale is the maximum amount that the UNet model downscales the latent image internally. + model_config = self._node_context.models.get_config(self._model_id.key) + if model_config.base == BaseModelType.StableDiffusion1: + self._max_unet_downscale = 8 + elif model_config.base == BaseModelType.StableDiffusionXL: + self._max_unet_downscale = 4 + else: + raise ValueError(f"Unexpected T2I-Adapter base model type: '{model_config.base}'.") + + @callback(ExtensionCallbackType.SETUP) + def setup(self, ctx: DenoiseContext): + t2i_model: T2IAdapter + with self._node_context.models.load(self._model_id) as t2i_model: + _, _, latents_height, latents_width = ctx.inputs.orig_latents.shape + + self._adapter_state = self._run_model( + model=t2i_model, + image=self._image, + latents_height=latents_height, + latents_width=latents_width, + ) + + def _run_model( + self, + model: T2IAdapter, + image: Image, + latents_height: int, + latents_width: int, + ): + # 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. + input_height = latents_height // self._max_unet_downscale * model.total_downscale_factor + input_width = latents_width // self._max_unet_downscale * 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). + t2i_image = prepare_control_image( + image=image, + do_classifier_free_guidance=False, + width=input_width, + height=input_height, + num_channels=model.config["in_channels"], + device=TorchDevice.choose_torch_device(), + dtype=model.dtype, + resize_mode=self._resize_mode, + ) + + return model(t2i_image) + + @callback(ExtensionCallbackType.PRE_UNET) + def pre_unet_step(self, ctx: DenoiseContext): + # skip if model not active in current step + total_steps = len(ctx.inputs.timesteps) + first_step = math.floor(self._begin_step_percent * total_steps) + last_step = math.ceil(self._end_step_percent * total_steps) + if ctx.step_index < first_step or ctx.step_index > last_step: + return + + weight = self._weight + if isinstance(weight, list): + weight = weight[ctx.step_index] + + adapter_state = self._adapter_state + if ctx.conditioning_mode == ConditioningMode.Both: + adapter_state = [torch.cat([v] * 2) for v in adapter_state] + + if ctx.unet_kwargs.down_intrablock_additional_residuals is None: + ctx.unet_kwargs.down_intrablock_additional_residuals = [v * weight for v in adapter_state] + else: + for i, value in enumerate(adapter_state): + ctx.unet_kwargs.down_intrablock_additional_residuals[i] += value * weight diff --git a/invokeai/backend/stable_diffusion/extensions_manager.py b/invokeai/backend/stable_diffusion/extensions_manager.py new file mode 100644 index 00000000000..3783bb422e5 --- /dev/null +++ b/invokeai/backend/stable_diffusion/extensions_manager.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from contextlib import ExitStack, contextmanager +from typing import TYPE_CHECKING, Callable, Dict, List, Optional + +import torch +from diffusers import UNet2DConditionModel + +from invokeai.app.services.session_processor.session_processor_common import CanceledException +from invokeai.backend.util.original_weights_storage import OriginalWeightsStorage + +if TYPE_CHECKING: + from invokeai.backend.stable_diffusion.denoise_context import DenoiseContext + from invokeai.backend.stable_diffusion.extension_callback_type import ExtensionCallbackType + from invokeai.backend.stable_diffusion.extensions.base import CallbackFunctionWithMetadata, ExtensionBase + + +class ExtensionsManager: + def __init__(self, is_canceled: Optional[Callable[[], bool]] = None): + self._is_canceled = is_canceled + + # A list of extensions in the order that they were added to the ExtensionsManager. + self._extensions: List[ExtensionBase] = [] + self._ordered_callbacks: Dict[ExtensionCallbackType, List[CallbackFunctionWithMetadata]] = {} + + def add_extension(self, extension: ExtensionBase): + self._extensions.append(extension) + self._regenerate_ordered_callbacks() + + def _regenerate_ordered_callbacks(self): + """Regenerates self._ordered_callbacks. Intended to be called each time a new extension is added.""" + self._ordered_callbacks = {} + + # Fill the ordered callbacks dictionary. + for extension in self._extensions: + for callback_type, callbacks in extension.get_callbacks().items(): + if callback_type not in self._ordered_callbacks: + self._ordered_callbacks[callback_type] = [] + self._ordered_callbacks[callback_type].extend(callbacks) + + # Sort each callback list. + for callback_type, callbacks in self._ordered_callbacks.items(): + # Note that sorted() is stable, so if two callbacks have the same order, the order that they extensions were + # added will be preserved. + self._ordered_callbacks[callback_type] = sorted(callbacks, key=lambda x: x.metadata.order) + + def run_callback(self, callback_type: ExtensionCallbackType, ctx: DenoiseContext): + if self._is_canceled and self._is_canceled(): + raise CanceledException + + callbacks = self._ordered_callbacks.get(callback_type, []) + for cb in callbacks: + cb.function(ctx) + + @contextmanager + def patch_extensions(self, ctx: DenoiseContext): + if self._is_canceled and self._is_canceled(): + raise CanceledException + + with ExitStack() as exit_stack: + for ext in self._extensions: + exit_stack.enter_context(ext.patch_extension(ctx)) + + yield None + + @contextmanager + def patch_unet(self, unet: UNet2DConditionModel, cached_weights: Optional[Dict[str, torch.Tensor]] = None): + if self._is_canceled and self._is_canceled(): + raise CanceledException + + original_weights = OriginalWeightsStorage(cached_weights) + try: + with ExitStack() as exit_stack: + for ext in self._extensions: + exit_stack.enter_context(ext.patch_unet(unet, original_weights)) + + yield None + + finally: + with torch.no_grad(): + for param_key, weight in original_weights.get_changed_weights(): + unet.get_parameter(param_key).copy_(weight) diff --git a/invokeai/backend/stable_diffusion/multi_diffusion_pipeline.py b/invokeai/backend/stable_diffusion/multi_diffusion_pipeline.py new file mode 100644 index 00000000000..63e74de5044 --- /dev/null +++ b/invokeai/backend/stable_diffusion/multi_diffusion_pipeline.py @@ -0,0 +1,194 @@ +from __future__ import annotations + +import copy +from dataclasses import dataclass +from typing import Any, Callable, Optional + +import torch +from diffusers.schedulers.scheduling_utils import SchedulerMixin + +from invokeai.backend.stable_diffusion.diffusers_pipeline import ( + ControlNetData, + PipelineIntermediateState, + StableDiffusionGeneratorPipeline, +) +from invokeai.backend.stable_diffusion.diffusion.conditioning_data import TextConditioningData +from invokeai.backend.tiles.utils import Tile + + +@dataclass +class MultiDiffusionRegionConditioning: + # Region coords in latent space. + region: Tile + text_conditioning_data: TextConditioningData + control_data: list[ControlNetData] + + +class MultiDiffusionPipeline(StableDiffusionGeneratorPipeline): + """A Stable Diffusion pipeline that uses Multi-Diffusion (https://arxiv.org/pdf/2302.08113) for denoising.""" + + def _check_regional_prompting(self, multi_diffusion_conditioning: list[MultiDiffusionRegionConditioning]): + """Validate that regional conditioning is not used.""" + for region_conditioning in multi_diffusion_conditioning: + if ( + region_conditioning.text_conditioning_data.cond_regions is not None + or region_conditioning.text_conditioning_data.uncond_regions is not None + ): + raise NotImplementedError("Regional prompting is not yet supported in Multi-Diffusion.") + + def multi_diffusion_denoise( + self, + multi_diffusion_conditioning: list[MultiDiffusionRegionConditioning], + target_overlap: int, + latents: torch.Tensor, + scheduler_step_kwargs: dict[str, Any], + noise: Optional[torch.Tensor], + timesteps: torch.Tensor, + init_timestep: torch.Tensor, + callback: Callable[[PipelineIntermediateState], None], + ) -> torch.Tensor: + self._check_regional_prompting(multi_diffusion_conditioning) + + if init_timestep.shape[0] == 0: + return latents + + batch_size, _, latent_height, latent_width = latents.shape + batched_init_timestep = init_timestep.expand(batch_size) + + # noise can be None if the latents have already been noised (e.g. when running the SDXL refiner). + if noise is not None: + # TODO(ryand): I'm pretty sure we should be applying init_noise_sigma in cases where we are starting with + # full noise. Investigate the history of why this got commented out. + # latents = noise * self.scheduler.init_noise_sigma # it's like in t2l according to diffusers + latents = self.scheduler.add_noise(latents, noise, batched_init_timestep) + assert isinstance(latents, torch.Tensor) # For static type checking. + + # TODO(ryand): Look into the implications of passing in latents here that are larger than they will be after + # cropping into regions. + self._adjust_memory_efficient_attention(latents) + + # Many of the diffusers schedulers are stateful (i.e. they update internal state in each call to step()). Since + # we are calling step() multiple times at the same timestep (once for each region batch), we must maintain a + # separate scheduler state for each region batch. + # TODO(ryand): This solution allows all schedulers to **run**, but does not fully solve the issue of scheduler + # statefulness. Some schedulers store previous model outputs in their state, but these values become incorrect + # as Multi-Diffusion blending is applied (e.g. the PNDMScheduler). This can result in a blurring effect when + # multiple MultiDiffusion regions overlap. Solving this properly would require a case-by-case review of each + # scheduler to determine how it's state needs to be updated for compatibilty with Multi-Diffusion. + region_batch_schedulers: list[SchedulerMixin] = [ + copy.deepcopy(self.scheduler) for _ in multi_diffusion_conditioning + ] + + callback( + PipelineIntermediateState( + step=0, + order=self.scheduler.order, + total_steps=len(timesteps), + timestep=self.scheduler.config.num_train_timesteps, + latents=latents, + ) + ) + + for i, t in enumerate(self.progress_bar(timesteps)): + batched_t = t.expand(batch_size) + + merged_latents = torch.zeros_like(latents) + merged_latents_weights = torch.zeros( + (1, 1, latent_height, latent_width), device=latents.device, dtype=latents.dtype + ) + merged_pred_original: torch.Tensor | None = None + for region_idx, region_conditioning in enumerate(multi_diffusion_conditioning): + # Switch to the scheduler for the region batch. + self.scheduler = region_batch_schedulers[region_idx] + + # Crop the inputs to the region. + region_latents = latents[ + :, + :, + region_conditioning.region.coords.top : region_conditioning.region.coords.bottom, + region_conditioning.region.coords.left : region_conditioning.region.coords.right, + ] + + # Run the denoising step on the region. + step_output = self.step( + t=batched_t, + latents=region_latents, + conditioning_data=region_conditioning.text_conditioning_data, + step_index=i, + total_step_count=len(timesteps), + scheduler_step_kwargs=scheduler_step_kwargs, + mask_guidance=None, + mask=None, + masked_latents=None, + control_data=region_conditioning.control_data, + ) + + # Build a region_weight matrix that applies gradient blending to the edges of the region. + region = region_conditioning.region + _, _, region_height, region_width = step_output.prev_sample.shape + region_weight = torch.ones( + (1, 1, region_height, region_width), + dtype=latents.dtype, + device=latents.device, + ) + if region.overlap.left > 0: + left_grad = torch.linspace( + 0, 1, region.overlap.left, device=latents.device, dtype=latents.dtype + ).view((1, 1, 1, -1)) + region_weight[:, :, :, : region.overlap.left] *= left_grad + if region.overlap.top > 0: + top_grad = torch.linspace( + 0, 1, region.overlap.top, device=latents.device, dtype=latents.dtype + ).view((1, 1, -1, 1)) + region_weight[:, :, : region.overlap.top, :] *= top_grad + if region.overlap.right > 0: + right_grad = torch.linspace( + 1, 0, region.overlap.right, device=latents.device, dtype=latents.dtype + ).view((1, 1, 1, -1)) + region_weight[:, :, :, -region.overlap.right :] *= right_grad + if region.overlap.bottom > 0: + bottom_grad = torch.linspace( + 1, 0, region.overlap.bottom, device=latents.device, dtype=latents.dtype + ).view((1, 1, -1, 1)) + region_weight[:, :, -region.overlap.bottom :, :] *= bottom_grad + + # Update the merged results with the region results. + merged_latents[ + :, :, region.coords.top : region.coords.bottom, region.coords.left : region.coords.right + ] += step_output.prev_sample * region_weight + merged_latents_weights[ + :, :, region.coords.top : region.coords.bottom, region.coords.left : region.coords.right + ] += region_weight + + pred_orig_sample = getattr(step_output, "pred_original_sample", None) + if pred_orig_sample is not None: + # If one region has pred_original_sample, then we can assume that all regions will have it, because + # they all use the same scheduler. + if merged_pred_original is None: + merged_pred_original = torch.zeros_like(latents) + merged_pred_original[ + :, :, region.coords.top : region.coords.bottom, region.coords.left : region.coords.right + ] += pred_orig_sample + + # Normalize the merged results. + latents = torch.where(merged_latents_weights > 0, merged_latents / merged_latents_weights, merged_latents) + # For debugging, uncomment this line to visualize the region seams: + # latents = torch.where(merged_latents_weights > 1, 0.0, latents) + predicted_original = None + if merged_pred_original is not None: + predicted_original = torch.where( + merged_latents_weights > 0, merged_pred_original / merged_latents_weights, merged_pred_original + ) + + callback( + PipelineIntermediateState( + step=i + 1, + order=self.scheduler.order, + total_steps=len(timesteps), + timestep=int(t), + latents=latents, + predicted_original=predicted_original, + ) + ) + + return latents diff --git a/invokeai/backend/stable_diffusion/schedulers/__init__.py b/invokeai/backend/stable_diffusion/schedulers/__init__.py new file mode 100644 index 00000000000..6c02acda512 --- /dev/null +++ b/invokeai/backend/stable_diffusion/schedulers/__init__.py @@ -0,0 +1,3 @@ +from invokeai.backend.stable_diffusion.schedulers.schedulers import SCHEDULER_MAP # noqa: F401 + +__all__ = ["SCHEDULER_MAP"] diff --git a/invokeai/backend/stable_diffusion/schedulers/schedulers.py b/invokeai/backend/stable_diffusion/schedulers/schedulers.py new file mode 100644 index 00000000000..c883767e82d --- /dev/null +++ b/invokeai/backend/stable_diffusion/schedulers/schedulers.py @@ -0,0 +1,103 @@ +from typing import Any, Literal, Type + +from diffusers import ( + DDIMScheduler, + DDPMScheduler, + DEISMultistepScheduler, + DPMSolverMultistepScheduler, + DPMSolverSDEScheduler, + DPMSolverSinglestepScheduler, + EulerAncestralDiscreteScheduler, + EulerDiscreteScheduler, + HeunDiscreteScheduler, + KDPM2AncestralDiscreteScheduler, + KDPM2DiscreteScheduler, + LCMScheduler, + LMSDiscreteScheduler, + PNDMScheduler, + TCDScheduler, + UniPCMultistepScheduler, +) +from diffusers.schedulers.scheduling_utils import SchedulerMixin + +from invokeai.backend.rectified_flow.er_sde_scheduler import ERSDEScheduler + +# TODO: add dpmpp_3s/dpmpp_3s_k when fix released +# https://github.com/huggingface/diffusers/issues/9007 + +SCHEDULER_NAME_VALUES = Literal[ + "ddim", + "ddpm", + "deis", + "deis_k", + "lms", + "lms_k", + "pndm", + "heun", + "heun_k", + "euler", + "euler_k", + "euler_a", + "kdpm_2", + "kdpm_2_k", + "kdpm_2_a", + "kdpm_2_a_k", + "dpmpp_2s", + "dpmpp_2s_k", + "dpmpp_2m", + "dpmpp_2m_k", + "dpmpp_2m_sde", + "dpmpp_2m_sde_k", + "dpmpp_3m", + "dpmpp_3m_k", + "dpmpp_sde", + "dpmpp_sde_k", + "er_sde", + "unipc", + "unipc_k", + "lcm", + "tcd", +] + +SCHEDULER_MAP: dict[SCHEDULER_NAME_VALUES, tuple[Type[SchedulerMixin], dict[str, Any]]] = { + "ddim": (DDIMScheduler, {}), + "ddpm": (DDPMScheduler, {}), + "deis": (DEISMultistepScheduler, {"use_karras_sigmas": False}), + "deis_k": (DEISMultistepScheduler, {"use_karras_sigmas": True}), + "lms": (LMSDiscreteScheduler, {"use_karras_sigmas": False}), + "lms_k": (LMSDiscreteScheduler, {"use_karras_sigmas": True}), + "pndm": (PNDMScheduler, {}), + "heun": (HeunDiscreteScheduler, {"use_karras_sigmas": False}), + "heun_k": (HeunDiscreteScheduler, {"use_karras_sigmas": True}), + "euler": (EulerDiscreteScheduler, {"use_karras_sigmas": False}), + "euler_k": (EulerDiscreteScheduler, {"use_karras_sigmas": True}), + "euler_a": (EulerAncestralDiscreteScheduler, {}), + "kdpm_2": (KDPM2DiscreteScheduler, {"use_karras_sigmas": False}), + "kdpm_2_k": (KDPM2DiscreteScheduler, {"use_karras_sigmas": True}), + "kdpm_2_a": (KDPM2AncestralDiscreteScheduler, {"use_karras_sigmas": False}), + "kdpm_2_a_k": (KDPM2AncestralDiscreteScheduler, {"use_karras_sigmas": True}), + "dpmpp_2s": (DPMSolverSinglestepScheduler, {"use_karras_sigmas": False, "solver_order": 2}), + "dpmpp_2s_k": (DPMSolverSinglestepScheduler, {"use_karras_sigmas": True, "solver_order": 2}), + "dpmpp_2m": (DPMSolverMultistepScheduler, {"use_karras_sigmas": False, "solver_order": 2}), + "dpmpp_2m_k": (DPMSolverMultistepScheduler, {"use_karras_sigmas": True, "solver_order": 2}), + "dpmpp_2m_sde": ( + DPMSolverMultistepScheduler, + {"use_karras_sigmas": False, "solver_order": 2, "algorithm_type": "sde-dpmsolver++"}, + ), + "dpmpp_2m_sde_k": ( + DPMSolverMultistepScheduler, + {"use_karras_sigmas": True, "solver_order": 2, "algorithm_type": "sde-dpmsolver++"}, + ), + "dpmpp_3m": (DPMSolverMultistepScheduler, {"use_karras_sigmas": False, "solver_order": 3}), + "dpmpp_3m_k": (DPMSolverMultistepScheduler, {"use_karras_sigmas": True, "solver_order": 3}), + "dpmpp_sde": (DPMSolverSDEScheduler, {"use_karras_sigmas": False, "noise_sampler_seed": 0}), + "dpmpp_sde_k": (DPMSolverSDEScheduler, {"use_karras_sigmas": True, "noise_sampler_seed": 0}), + "er_sde": ( + ERSDEScheduler, + {"solver_order": 3, "use_flow_sigmas": False, "stochastic": True}, + ), + "unipc": (UniPCMultistepScheduler, {"use_karras_sigmas": False, "cpu_only": True}), + "unipc_k": (UniPCMultistepScheduler, {"use_karras_sigmas": True, "cpu_only": True}), + "lcm": (LCMScheduler, {}), + "tcd": (TCDScheduler, {}), +} diff --git a/invokeai/backend/stable_diffusion/vae_tiling.py b/invokeai/backend/stable_diffusion/vae_tiling.py new file mode 100644 index 00000000000..d31cb331f43 --- /dev/null +++ b/invokeai/backend/stable_diffusion/vae_tiling.py @@ -0,0 +1,35 @@ +from contextlib import contextmanager + +from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL +from diffusers.models.autoencoders.autoencoder_tiny import AutoencoderTiny + + +@contextmanager +def patch_vae_tiling_params( + vae: AutoencoderKL | AutoencoderTiny, + tile_sample_min_size: int, + tile_latent_min_size: int, + tile_overlap_factor: float, +): + """Patch the parameters that control the VAE tiling tile size and overlap. + + These parameters are not explicitly exposed in the VAE's API, but they have a significant impact on the quality of + the outputs. As a general rule, bigger tiles produce better results, but this comes at the cost of higher memory + usage. + """ + # Record initial config. + orig_tile_sample_min_size = vae.tile_sample_min_size + orig_tile_latent_min_size = vae.tile_latent_min_size + orig_tile_overlap_factor = vae.tile_overlap_factor + + try: + # Apply target config. + vae.tile_sample_min_size = tile_sample_min_size + vae.tile_latent_min_size = tile_latent_min_size + vae.tile_overlap_factor = tile_overlap_factor + yield + finally: + # Restore initial config. + vae.tile_sample_min_size = orig_tile_sample_min_size + vae.tile_latent_min_size = orig_tile_latent_min_size + vae.tile_overlap_factor = orig_tile_overlap_factor diff --git a/invokeai/backend/text_llm_pipeline.py b/invokeai/backend/text_llm_pipeline.py new file mode 100644 index 00000000000..69815c1a7f7 --- /dev/null +++ b/invokeai/backend/text_llm_pipeline.py @@ -0,0 +1,56 @@ +import torch +from transformers import PreTrainedModel, PreTrainedTokenizerBase + +DEFAULT_SYSTEM_PROMPT = ( + "You are an expert prompt writer for AI image generation. " + "Given a brief description, expand it into a detailed, vivid prompt suitable for generating high-quality images. " + "Only output the expanded prompt, nothing else." +) + + +class TextLLMPipeline: + """A wrapper for a causal language model + tokenizer for text generation.""" + + def __init__(self, model: PreTrainedModel, tokenizer: PreTrainedTokenizerBase): + self._model = model + self._tokenizer = tokenizer + + def run( + self, + prompt: str, + system_prompt: str = DEFAULT_SYSTEM_PROMPT, + max_new_tokens: int = 300, + device: torch.device = torch.device("cpu"), + dtype: torch.dtype = torch.float16, + ) -> str: + # Build messages for chat template if supported, otherwise use raw prompt. + if hasattr(self._tokenizer, "apply_chat_template") and self._tokenizer.chat_template is not None: + messages = [] + if system_prompt: + messages.append({"role": "system", "content": system_prompt}) + messages.append({"role": "user", "content": prompt}) + formatted_prompt: str = self._tokenizer.apply_chat_template( + messages, tokenize=False, add_generation_prompt=True + ) + else: + # Fallback for models without chat template + if system_prompt: + formatted_prompt = f"{system_prompt}\n\nUser: {prompt}\nAssistant:" + else: + formatted_prompt = prompt + + inputs = self._tokenizer(formatted_prompt, return_tensors="pt").to(device=device) + output = self._model.generate( + **inputs, + max_new_tokens=max_new_tokens, + do_sample=True, + temperature=0.7, + top_p=0.9, + ) + + # Decode only the newly generated tokens (exclude the input prompt tokens). + input_length = inputs["input_ids"].shape[1] + generated_tokens = output[0][input_length:] + response = self._tokenizer.decode(generated_tokens, skip_special_tokens=True).strip() + + return response diff --git a/invokeai/backend/textual_inversion.py b/invokeai/backend/textual_inversion.py new file mode 100644 index 00000000000..b83d769a8d1 --- /dev/null +++ b/invokeai/backend/textual_inversion.py @@ -0,0 +1,129 @@ +"""Textual Inversion wrapper class.""" + +from pathlib import Path +from typing import Optional, Union + +import torch +from compel.embeddings_provider import BaseTextualInversionManager +from safetensors.torch import load_file +from transformers import CLIPTokenizer +from typing_extensions import Self + +from invokeai.backend.raw_model import RawModel +from invokeai.backend.util.calc_tensor_size import calc_tensors_size + + +class TextualInversionModelRaw(RawModel): + embedding: torch.Tensor # [n, 768]|[n, 1280] + embedding_2: Optional[torch.Tensor] = None # [n, 768]|[n, 1280] - for SDXL models + + @classmethod + def from_checkpoint( + cls, + file_path: Union[str, Path], + device: Optional[torch.device] = None, + dtype: Optional[torch.dtype] = None, + ) -> Self: + if not isinstance(file_path, Path): + file_path = Path(file_path) + + result = cls() # TODO: + + if file_path.suffix == ".safetensors": + state_dict = load_file(file_path.absolute().as_posix(), device="cpu") + else: + state_dict = torch.load(file_path, map_location="cpu") + + # both v1 and v2 format embeddings + # difference mostly in metadata + if "string_to_param" in state_dict: + if len(state_dict["string_to_param"]) > 1: + print( + f'Warn: Embedding "{file_path.name}" contains multiple tokens, which is not supported. The first', + " token will be used.", + ) + + result.embedding = next(iter(state_dict["string_to_param"].values())) + + # v3 (easynegative) + elif "emb_params" in state_dict: + result.embedding = state_dict["emb_params"] + + # v5(sdxl safetensors file) + elif "clip_g" in state_dict and "clip_l" in state_dict: + result.embedding = state_dict["clip_g"] + result.embedding_2 = state_dict["clip_l"] + + # v4(diffusers bin files) + else: + result.embedding = next(iter(state_dict.values())) + + if len(result.embedding.shape) == 1: + result.embedding = result.embedding.unsqueeze(0) + + if not isinstance(result.embedding, torch.Tensor): + raise ValueError(f"Invalid embeddings file: {file_path.name}") + + return result + + def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None: + if not torch.cuda.is_available(): + return + for emb in [self.embedding, self.embedding_2]: + if emb is not None: + emb.to(device=device, dtype=dtype) + + def calc_size(self) -> int: + """Get the size of this model in bytes.""" + return calc_tensors_size([self.embedding, self.embedding_2]) + + +class TextualInversionManager(BaseTextualInversionManager): + """TextualInversionManager implements the BaseTextualInversionManager ABC from the compel library.""" + + def __init__(self, tokenizer: CLIPTokenizer): + self.pad_tokens: dict[int, list[int]] = {} + self.tokenizer = tokenizer + + def expand_textual_inversion_token_ids_if_necessary(self, token_ids: list[int]) -> list[int]: + """Given a list of tokens ids, expand any TI tokens to their corresponding pad tokens. + + For example, suppose we have a `` TI with 4 vectors that was added to the tokenizer with the following + mapping of tokens to token_ids: + ``` + : 49408 + : 49409 + : 49410 + : 49411 + ``` + `self.pad_tokens` would be set to `{49408: [49408, 49409, 49410, 49411]}`. + This function is responsible for expanding `49408` in the token_ids list to `[49408, 49409, 49410, 49411]`. + """ + # Short circuit if there are no pad tokens to save a little time. + if len(self.pad_tokens) == 0: + return token_ids + + # This function assumes that compel has not included the BOS and EOS tokens in the token_ids list. We verify + # this assumption here. + if token_ids[0] == self.tokenizer.bos_token_id: + raise ValueError("token_ids must not start with bos_token_id") + if token_ids[-1] == self.tokenizer.eos_token_id: + raise ValueError("token_ids must not end with eos_token_id") + + # Expand any TI tokens to their corresponding pad tokens. + new_token_ids: list[int] = [] + for token_id in token_ids: + new_token_ids.append(token_id) + if token_id in self.pad_tokens: + new_token_ids.extend(self.pad_tokens[token_id]) + + # Do not exceed the max model input size. The -2 here is compensating for + # compel.embeddings_provider.get_token_ids(), which first removes and then adds back the start and end tokens. + max_length = self.tokenizer.model_max_length - 2 + if len(new_token_ids) > max_length: + # HACK: If TI token expansion causes us to exceed the max text encoder input length, we silently discard + # tokens. Token expansion should happen in a way that is compatible with compel's default handling of long + # prompts. + new_token_ids = new_token_ids[0:max_length] + + return new_token_ids diff --git a/invokeai/backend/tiles/__init__.py b/invokeai/backend/tiles/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/backend/tiles/tiles.py b/invokeai/backend/tiles/tiles.py new file mode 100644 index 00000000000..2757dadba20 --- /dev/null +++ b/invokeai/backend/tiles/tiles.py @@ -0,0 +1,426 @@ +import math +from typing import Union + +import numpy as np + +from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR +from invokeai.backend.tiles.utils import TBLR, Tile, paste, seam_blend + + +def calc_overlap(tiles: list[Tile], num_tiles_x: int, num_tiles_y: int) -> list[Tile]: + """Calculate and update the overlap of a list of tiles. + + Args: + tiles (list[Tile]): The list of tiles describing the locations of the respective `tile_images`. + num_tiles_x: the number of tiles on the x axis. + num_tiles_y: the number of tiles on the y axis. + """ + + def get_tile_or_none(idx_y: int, idx_x: int) -> Union[Tile, None]: + if idx_y < 0 or idx_y > num_tiles_y or idx_x < 0 or idx_x > num_tiles_x: + return None + return tiles[idx_y * num_tiles_x + idx_x] + + for tile_idx_y in range(num_tiles_y): + for tile_idx_x in range(num_tiles_x): + cur_tile = get_tile_or_none(tile_idx_y, tile_idx_x) + top_neighbor_tile = get_tile_or_none(tile_idx_y - 1, tile_idx_x) + left_neighbor_tile = get_tile_or_none(tile_idx_y, tile_idx_x - 1) + + assert cur_tile is not None + + # Update cur_tile top-overlap and corresponding top-neighbor bottom-overlap. + if top_neighbor_tile is not None: + cur_tile.overlap.top = max(0, top_neighbor_tile.coords.bottom - cur_tile.coords.top) + top_neighbor_tile.overlap.bottom = cur_tile.overlap.top + + # Update cur_tile left-overlap and corresponding left-neighbor right-overlap. + if left_neighbor_tile is not None: + cur_tile.overlap.left = max(0, left_neighbor_tile.coords.right - cur_tile.coords.left) + left_neighbor_tile.overlap.right = cur_tile.overlap.left + return tiles + + +def calc_tiles_with_overlap( + image_height: int, image_width: int, tile_height: int, tile_width: int, overlap: int = 0 +) -> list[Tile]: + """Calculate the tile coordinates for a given image shape under a simple tiling scheme with overlaps. + + Args: + image_height (int): The image height in px. + image_width (int): The image width in px. + tile_height (int): The tile height in px. All tiles will have this height. + tile_width (int): The tile width in px. All tiles will have this width. + overlap (int, optional): The target overlap between adjacent tiles. If the tiles do not evenly cover the image + shape, then the last row/column of tiles will overlap more than this. Defaults to 0. + + Returns: + list[Tile]: A list of tiles that cover the image shape. Ordered from left-to-right, top-to-bottom. + """ + assert image_height >= tile_height + assert image_width >= tile_width + assert overlap < tile_height + assert overlap < tile_width + + non_overlap_per_tile_height = tile_height - overlap + non_overlap_per_tile_width = tile_width - overlap + + num_tiles_y = math.ceil((image_height - overlap) / non_overlap_per_tile_height) + num_tiles_x = math.ceil((image_width - overlap) / non_overlap_per_tile_width) + + # tiles[y * num_tiles_x + x] is the tile for the y'th row, x'th column. + tiles: list[Tile] = [] + + # Calculate tile coordinates. (Ignore overlap values for now.) + for tile_idx_y in range(num_tiles_y): + for tile_idx_x in range(num_tiles_x): + tile = Tile( + coords=TBLR( + top=tile_idx_y * non_overlap_per_tile_height, + bottom=tile_idx_y * non_overlap_per_tile_height + tile_height, + left=tile_idx_x * non_overlap_per_tile_width, + right=tile_idx_x * non_overlap_per_tile_width + tile_width, + ), + overlap=TBLR(top=0, bottom=0, left=0, right=0), + ) + + if tile.coords.bottom > image_height: + # If this tile would go off the bottom of the image, shift it so that it is aligned with the bottom + # of the image. + tile.coords.bottom = image_height + tile.coords.top = image_height - tile_height + + if tile.coords.right > image_width: + # If this tile would go off the right edge of the image, shift it so that it is aligned with the + # right edge of the image. + tile.coords.right = image_width + tile.coords.left = image_width - tile_width + + tiles.append(tile) + + return calc_overlap(tiles, num_tiles_x, num_tiles_y) + + +def calc_tiles_even_split( + image_height: int, image_width: int, num_tiles_x: int, num_tiles_y: int, overlap: int = 0 +) -> list[Tile]: + """Calculate the tile coordinates for a given image shape with the number of tiles requested. + + Args: + image_height (int): The image height in px. + image_width (int): The image width in px. + num_x_tiles (int): The number of tile to split the image into on the X-axis. + num_y_tiles (int): The number of tile to split the image into on the Y-axis. + overlap (int, optional): The overlap between adjacent tiles in pixels. Defaults to 0. + + Returns: + list[Tile]: A list of tiles that cover the image shape. Ordered from left-to-right, top-to-bottom. + """ + # Ensure the image is divisible by LATENT_SCALE_FACTOR + if image_width % LATENT_SCALE_FACTOR != 0 or image_height % LATENT_SCALE_FACTOR != 0: + raise ValueError(f"image size (({image_width}, {image_height})) must be divisible by {LATENT_SCALE_FACTOR}") + + # Calculate the tile size based on the number of tiles and overlap, and ensure it's divisible by 8 (rounding down) + if num_tiles_x > 1: + # ensure the overlap is not more than the maximum overlap if we only have 1 tile then we dont care about overlap + assert overlap <= image_width - (LATENT_SCALE_FACTOR * (num_tiles_x - 1)) + tile_size_x = LATENT_SCALE_FACTOR * math.floor( + ((image_width + overlap * (num_tiles_x - 1)) // num_tiles_x) / LATENT_SCALE_FACTOR + ) + assert overlap < tile_size_x + else: + tile_size_x = image_width + + if num_tiles_y > 1: + # ensure the overlap is not more than the maximum overlap if we only have 1 tile then we dont care about overlap + assert overlap <= image_height - (LATENT_SCALE_FACTOR * (num_tiles_y - 1)) + tile_size_y = LATENT_SCALE_FACTOR * math.floor( + ((image_height + overlap * (num_tiles_y - 1)) // num_tiles_y) / LATENT_SCALE_FACTOR + ) + assert overlap < tile_size_y + else: + tile_size_y = image_height + + # tiles[y * num_tiles_x + x] is the tile for the y'th row, x'th column. + tiles: list[Tile] = [] + + # Calculate tile coordinates. (Ignore overlap values for now.) + for tile_idx_y in range(num_tiles_y): + # Calculate the top and bottom of the row + top = tile_idx_y * (tile_size_y - overlap) + bottom = min(top + tile_size_y, image_height) + # For the last row adjust bottom to be the height of the image + if tile_idx_y == num_tiles_y - 1: + bottom = image_height + + for tile_idx_x in range(num_tiles_x): + # Calculate the left & right coordinate of each tile + left = tile_idx_x * (tile_size_x - overlap) + right = min(left + tile_size_x, image_width) + # For the last tile in the row adjust right to be the width of the image + if tile_idx_x == num_tiles_x - 1: + right = image_width + + tile = Tile( + coords=TBLR(top=top, bottom=bottom, left=left, right=right), + overlap=TBLR(top=0, bottom=0, left=0, right=0), + ) + + tiles.append(tile) + + return calc_overlap(tiles, num_tiles_x, num_tiles_y) + + +def calc_tiles_min_overlap( + image_height: int, + image_width: int, + tile_height: int, + tile_width: int, + min_overlap: int = 0, +) -> list[Tile]: + """Calculate the tile coordinates for a given image shape under a simple tiling scheme with overlaps. + + Args: + image_height (int): The image height in px. + image_width (int): The image width in px. + tile_height (int): The tile height in px. All tiles will have this height. + tile_width (int): The tile width in px. All tiles will have this width. + min_overlap (int): The target minimum overlap between adjacent tiles. If the tiles do not evenly cover the image + shape, then the overlap will be spread between the tiles. + + Returns: + list[Tile]: A list of tiles that cover the image shape. Ordered from left-to-right, top-to-bottom. + """ + + assert min_overlap < tile_height + assert min_overlap < tile_width + + # catches the cases when the tile size is larger than the images size and adjusts the tile size + if image_width < tile_width: + tile_width = image_width + + if image_height < tile_height: + tile_height = image_height + + num_tiles_x = math.ceil((image_width - min_overlap) / (tile_width - min_overlap)) + num_tiles_y = math.ceil((image_height - min_overlap) / (tile_height - min_overlap)) + + # tiles[y * num_tiles_x + x] is the tile for the y'th row, x'th column. + tiles: list[Tile] = [] + + # Calculate tile coordinates. (Ignore overlap values for now.) + for tile_idx_y in range(num_tiles_y): + top = (tile_idx_y * (image_height - tile_height)) // (num_tiles_y - 1) if num_tiles_y > 1 else 0 + bottom = top + tile_height + + for tile_idx_x in range(num_tiles_x): + left = (tile_idx_x * (image_width - tile_width)) // (num_tiles_x - 1) if num_tiles_x > 1 else 0 + right = left + tile_width + + tile = Tile( + coords=TBLR(top=top, bottom=bottom, left=left, right=right), + overlap=TBLR(top=0, bottom=0, left=0, right=0), + ) + + tiles.append(tile) + + return calc_overlap(tiles, num_tiles_x, num_tiles_y) + + +def merge_tiles_with_linear_blending( + dst_image: np.ndarray, tiles: list[Tile], tile_images: list[np.ndarray], blend_amount: int +): + """Merge a set of image tiles into `dst_image` with linear blending between the tiles. + + We expect every tile edge to either: + 1) have an overlap of 0, because it is aligned with the image edge, or + 2) have an overlap >= blend_amount. + If neither of these conditions are satisfied, we raise an exception. + + The linear blending is centered at the halfway point of the overlap between adjacent tiles. + + Args: + dst_image (np.ndarray): The destination image. Shape: (H, W, C). + tiles (list[Tile]): The list of tiles describing the locations of the respective `tile_images`. + tile_images (list[np.ndarray]): The tile images to merge into `dst_image`. + blend_amount (int): The amount of blending (in px) between adjacent overlapping tiles. + """ + # Sort tiles and images 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_and_images = list(zip(tiles, tile_images, strict=True)) + tiles_and_images = sorted(tiles_and_images, key=lambda x: x[0].coords.left) + tiles_and_images = sorted(tiles_and_images, key=lambda x: x[0].coords.top) + + # Organize tiles into rows. + tile_and_image_rows: list[list[tuple[Tile, np.ndarray]]] = [] + cur_tile_and_image_row: list[tuple[Tile, np.ndarray]] = [] + first_tile_in_cur_row, _ = tiles_and_images[0] + for tile_and_image in tiles_and_images: + tile, _ = tile_and_image + if not ( + tile.coords.top == first_tile_in_cur_row.coords.top + and tile.coords.bottom == first_tile_in_cur_row.coords.bottom + ): + # Store the previous row, and start a new one. + tile_and_image_rows.append(cur_tile_and_image_row) + cur_tile_and_image_row = [] + first_tile_in_cur_row, _ = tile_and_image + + cur_tile_and_image_row.append(tile_and_image) + tile_and_image_rows.append(cur_tile_and_image_row) + + # Prepare 1D linear gradients for blending. + gradient_left_x = np.linspace(start=0.0, stop=1.0, num=blend_amount) + gradient_top_y = np.linspace(start=0.0, stop=1.0, num=blend_amount) + # Convert shape: (blend_amount, ) -> (blend_amount, 1). The extra dimension enables the gradient to be applied + # to a 2D image via broadcasting. Note that no additional dimension is needed on gradient_left_x for + # broadcasting to work correctly. + gradient_top_y = np.expand_dims(gradient_top_y, axis=1) + + for tile_and_image_row in tile_and_image_rows: + first_tile_in_row, _ = tile_and_image_row[0] + row_height = first_tile_in_row.coords.bottom - first_tile_in_row.coords.top + row_image = np.zeros((row_height, dst_image.shape[1], dst_image.shape[2]), dtype=dst_image.dtype) + + # Blend the tiles in the row horizontally. + for tile, tile_image in tile_and_image_row: + # We expect the tiles to be ordered left-to-right. For each tile, we construct a mask that applies linear + # blending to the left of the current tile. The inverse linear blending is automatically applied to the + # right of the tiles that have already been pasted by the paste(...) operation. + tile_height, tile_width, _ = tile_image.shape + mask = np.ones(shape=(tile_height, tile_width), dtype=np.float64) + + # Left blending: + if tile.overlap.left > 0: + assert tile.overlap.left >= blend_amount + # Center the blending gradient in the middle of the overlap. + blend_start_left = tile.overlap.left // 2 - blend_amount // 2 + # The region left of the blending region is masked completely. + mask[:, :blend_start_left] = 0.0 + # Apply the blend gradient to the mask. + mask[:, blend_start_left : blend_start_left + blend_amount] = gradient_left_x + # For visual debugging: + # tile_image[:, blend_start_left : blend_start_left + blend_amount] = 0 + + paste( + dst_image=row_image, + src_image=tile_image, + box=TBLR( + top=0, bottom=tile.coords.bottom - tile.coords.top, left=tile.coords.left, right=tile.coords.right + ), + mask=mask, + ) + + # Blend the row into the dst_image vertically. + # We construct a mask that applies linear blending to the top of the current row. The inverse linear blending is + # automatically applied to the bottom of the tiles that have already been pasted by the paste(...) operation. + mask = np.ones(shape=(row_image.shape[0], row_image.shape[1]), dtype=np.float64) + # Top blending: + # (See comments under 'Left blending' for an explanation of the logic.) + # We assume that the entire row has the same vertical overlaps as the first_tile_in_row. + if first_tile_in_row.overlap.top > 0: + assert first_tile_in_row.overlap.top >= blend_amount + blend_start_top = first_tile_in_row.overlap.top // 2 - blend_amount // 2 + mask[:blend_start_top, :] = 0.0 + mask[blend_start_top : blend_start_top + blend_amount, :] = gradient_top_y + # For visual debugging: + # row_image[blend_start_top : blend_start_top + blend_amount, :] = 0 + paste( + dst_image=dst_image, + src_image=row_image, + box=TBLR( + top=first_tile_in_row.coords.top, + bottom=first_tile_in_row.coords.bottom, + left=0, + right=row_image.shape[1], + ), + mask=mask, + ) + + +def merge_tiles_with_seam_blending( + dst_image: np.ndarray, tiles: list[Tile], tile_images: list[np.ndarray], blend_amount: int +): + """Merge a set of image tiles into `dst_image` with seam blending between the tiles. + + We expect every tile edge to either: + 1) have an overlap of 0, because it is aligned with the image edge, or + 2) have an overlap >= blend_amount. + If neither of these conditions are satisfied, we raise an exception. + + The seam blending is centered on a seam of least energy of the overlap between adjacent tiles. + + Args: + dst_image (np.ndarray): The destination image. Shape: (H, W, C). + tiles (list[Tile]): The list of tiles describing the locations of the respective `tile_images`. + tile_images (list[np.ndarray]): The tile images to merge into `dst_image`. + blend_amount (int): The amount of blending (in px) between adjacent overlapping tiles. + """ + # Sort tiles and images 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_and_images = list(zip(tiles, tile_images, strict=True)) + tiles_and_images = sorted(tiles_and_images, key=lambda x: x[0].coords.left) + tiles_and_images = sorted(tiles_and_images, key=lambda x: x[0].coords.top) + + # Organize tiles into rows. + tile_and_image_rows: list[list[tuple[Tile, np.ndarray]]] = [] + cur_tile_and_image_row: list[tuple[Tile, np.ndarray]] = [] + first_tile_in_cur_row, _ = tiles_and_images[0] + for tile_and_image in tiles_and_images: + tile, _ = tile_and_image + if not ( + tile.coords.top == first_tile_in_cur_row.coords.top + and tile.coords.bottom == first_tile_in_cur_row.coords.bottom + ): + # Store the previous row, and start a new one. + tile_and_image_rows.append(cur_tile_and_image_row) + cur_tile_and_image_row = [] + first_tile_in_cur_row, _ = tile_and_image + + cur_tile_and_image_row.append(tile_and_image) + tile_and_image_rows.append(cur_tile_and_image_row) + + for tile_and_image_row in tile_and_image_rows: + first_tile_in_row, _ = tile_and_image_row[0] + row_height = first_tile_in_row.coords.bottom - first_tile_in_row.coords.top + row_image = np.zeros((row_height, dst_image.shape[1], dst_image.shape[2]), dtype=dst_image.dtype) + + # Blend the tiles in the row horizontally. + for tile, tile_image in tile_and_image_row: + # We expect the tiles to be ordered left-to-right. + # For each tile: + # - extract the overlap regions and pass to seam_blend() + # - apply blended region to the row_image + # - apply the un-blended region to the row_image + tile_height, tile_width, _ = tile_image.shape + overlap_size = tile.overlap.left + # Left blending: + if overlap_size > 0: + assert overlap_size >= blend_amount + + overlap_coord_right = tile.coords.left + overlap_size + src_overlap = row_image[:, tile.coords.left : overlap_coord_right] + dst_overlap = tile_image[:, :overlap_size] + blended_overlap = seam_blend(src_overlap, dst_overlap, blend_amount, x_seam=False) + row_image[:, tile.coords.left : overlap_coord_right] = blended_overlap + row_image[:, overlap_coord_right : tile.coords.right] = tile_image[:, overlap_size:] + else: + # no overlap just paste the tile + row_image[:, tile.coords.left : tile.coords.right] = tile_image + + # Blend the row into the dst_image + # We assume that the entire row has the same vertical overlaps as the first_tile_in_row. + # Rows are processed in the same way as tiles (extract overlap, blend, apply) + row_overlap_size = first_tile_in_row.overlap.top + if row_overlap_size > 0: + assert row_overlap_size >= blend_amount + + overlap_coords_bottom = first_tile_in_row.coords.top + row_overlap_size + src_overlap = dst_image[first_tile_in_row.coords.top : overlap_coords_bottom, :] + dst_overlap = row_image[:row_overlap_size, :] + blended_overlap = seam_blend(src_overlap, dst_overlap, blend_amount, x_seam=True) + dst_image[first_tile_in_row.coords.top : overlap_coords_bottom, :] = blended_overlap + dst_image[overlap_coords_bottom : first_tile_in_row.coords.bottom, :] = row_image[row_overlap_size:, :] + else: + # no overlap just paste the row + dst_image[first_tile_in_row.coords.top : first_tile_in_row.coords.bottom, :] = row_image diff --git a/invokeai/backend/tiles/utils.py b/invokeai/backend/tiles/utils.py new file mode 100644 index 00000000000..dc6d914170e --- /dev/null +++ b/invokeai/backend/tiles/utils.py @@ -0,0 +1,152 @@ +import math +from typing import Optional + +import cv2 +import numpy as np +from pydantic import BaseModel, Field + + +class TBLR(BaseModel): + top: int + bottom: int + left: int + right: int + + def __eq__(self, other): + return ( + self.top == other.top + and self.bottom == other.bottom + and self.left == other.left + and self.right == other.right + ) + + +class Tile(BaseModel): + coords: TBLR = Field(description="The coordinates of this tile relative to its parent image.") + overlap: TBLR = Field(description="The amount of overlap with adjacent tiles on each side of this tile.") + + def __eq__(self, other): + return self.coords == other.coords and self.overlap == other.overlap + + +def paste(dst_image: np.ndarray, src_image: np.ndarray, box: TBLR, mask: Optional[np.ndarray] = None): + """Paste a source image into a destination image. + + Args: + dst_image (np.array): The destination image to paste into. Shape: (H, W, C). + src_image (np.array): The source image to paste. Shape: (H, W, C). H and W must be compatible with 'box'. + box (TBLR): Box defining the region in the 'dst_image' where 'src_image' will be pasted. + mask (Optional[np.array]): A mask that defines the blending between 'src_image' and 'dst_image'. + Range: [0.0, 1.0], Shape: (H, W). The output is calculate per-pixel according to + `src * mask + dst * (1 - mask)`. + """ + + if mask is None: + dst_image[box.top : box.bottom, box.left : box.right] = src_image + else: + mask = np.expand_dims(mask, -1) + dst_image_box = dst_image[box.top : box.bottom, box.left : box.right] + dst_image[box.top : box.bottom, box.left : box.right] = src_image * mask + dst_image_box * (1.0 - mask) + + +def seam_blend(ia1: np.ndarray, ia2: np.ndarray, blend_amount: int, x_seam: bool) -> np.ndarray: + """Blend two overlapping tile sections using a seams to find a path. + + It is assumed that input images will be RGB np arrays and are the same size. + + Args: + ia1 (np.array): Image array 1 Shape: (H, W, C). + ia2 (np.array): Image array 2 Shape: (H, W, C). + x_seam (bool): If the images should be blended on the x axis or not. + blend_amount (int): The size of the blur to use on the seam. Half of this value will be used to avoid the edges of the image. + """ + assert ia1.shape == ia2.shape + assert ia2.size == ia2.size + + def shift(arr, num, fill_value=255.0): + result = np.full_like(arr, fill_value) + if num > 0: + result[num:] = arr[:-num] + elif num < 0: + result[:num] = arr[-num:] + else: + result[:] = arr + return result + + # Assume RGB and convert to grey + # Could offer other options for the luminance conversion + # BT.709 [0.2126, 0.7152, 0.0722], BT.2020 [0.2627, 0.6780, 0.0593]) + # it might not have a huge impact due to the blur that is applied over the seam + iag1 = np.dot(ia1, [0.2989, 0.5870, 0.1140]) # BT.601 perceived brightness + iag2 = np.dot(ia2, [0.2989, 0.5870, 0.1140]) + + # Calc Difference between the images + ia = iag2 - iag1 + + # If the seam is on the X-axis rotate the array so we can treat it like a vertical seam + if x_seam: + ia = np.rot90(ia, 1) + + # Calc max and min X & Y limits + # gutter is used to avoid the blur hitting the edge of the image + gutter = math.ceil(blend_amount / 2) if blend_amount > 0 else 0 + max_y, max_x = ia.shape + max_x -= gutter + min_x = gutter + + # Calc the energy in the difference + # Could offer different energy calculations e.g. Sobel or Scharr + energy = np.abs(np.gradient(ia, axis=0)) + np.abs(np.gradient(ia, axis=1)) + + # Find the starting position of the seam + res = np.copy(energy) + for y in range(1, max_y): + row = res[y, :] + rowl = shift(row, -1) + rowr = shift(row, 1) + res[y, :] = res[y - 1, :] + np.min([row, rowl, rowr], axis=0) + + # create an array max_y long + lowest_energy_line = np.empty([max_y], dtype="uint16") + lowest_energy_line[max_y - 1] = np.argmin(res[max_y - 1, min_x : max_x - 1]) + + # Calc the path of the seam + # could offer options for larger search than just 1 pixel by adjusting lpos and rpos + for ypos in range(max_y - 2, -1, -1): + lowest_pos = lowest_energy_line[ypos + 1] + lpos = lowest_pos - 1 + rpos = lowest_pos + 1 + lpos = np.clip(lpos, min_x, max_x - 1) + rpos = np.clip(rpos, min_x, max_x - 1) + lowest_energy_line[ypos] = np.argmin(energy[ypos, lpos : rpos + 1]) + lpos + + # Draw the mask + mask = np.zeros_like(ia) + for ypos in range(0, max_y): + to_fill = lowest_energy_line[ypos] + mask[ypos, :to_fill] = 1 + + # If the seam is on the X-axis rotate the array back + if x_seam: + mask = np.rot90(mask, 3) + + # blur the seam mask if required + if blend_amount > 0: + mask = cv2.blur(mask, (blend_amount, blend_amount)) + + # for visual debugging + # from PIL import Image + # m_image = Image.fromarray((mask * 255.0).astype("uint8")) + + # copy ia2 over ia1 while applying the seam mask + mask = np.expand_dims(mask, -1) + blended_image = ia1 * mask + ia2 * (1.0 - mask) + + # for visual debugging + # i1 = Image.fromarray(ia1.astype("uint8")) + # i2 = Image.fromarray(ia2.astype("uint8")) + # b_image = Image.fromarray(blended_image.astype("uint8")) + # print(f"{ia1.shape}, {ia2.shape}, {mask.shape}, {blended_image.shape}") + # print(f"{i1.size}, {i2.size}, {m_image.size}, {b_image.size}") + + return blended_image diff --git a/invokeai/backend/util/__init__.py b/invokeai/backend/util/__init__.py new file mode 100644 index 00000000000..f24b6db3e12 --- /dev/null +++ b/invokeai/backend/util/__init__.py @@ -0,0 +1,12 @@ +""" +Initialization file for invokeai.backend.util +""" + +from invokeai.backend.util.logging import InvokeAILogger +from invokeai.backend.util.util import Chdir, directory_size + +__all__ = [ + "directory_size", + "Chdir", + "InvokeAILogger", +] diff --git a/invokeai/backend/util/attention.py b/invokeai/backend/util/attention.py new file mode 100644 index 00000000000..88dc6e5cec9 --- /dev/null +++ b/invokeai/backend/util/attention.py @@ -0,0 +1,33 @@ +# Copyright (c) 2023 Lincoln Stein and the InvokeAI Team +""" +Utility routine used for autodetection of optimal slice size +for attention mechanism. +""" + +import psutil +import torch + + +def auto_detect_slice_size(latents: torch.Tensor) -> str: + bytes_per_element_needed_for_baddbmm_duplication = latents.element_size() + 4 + max_size_required_for_baddbmm = ( + 16 + * latents.size(dim=2) + * latents.size(dim=3) + * latents.size(dim=2) + * latents.size(dim=3) + * bytes_per_element_needed_for_baddbmm_duplication + ) + if latents.device.type in {"cpu", "mps"}: + mem_free = psutil.virtual_memory().free + elif latents.device.type == "cuda": + mem_free, _ = torch.cuda.mem_get_info(latents.device) + else: + raise ValueError(f"unrecognized device {latents.device}") + + if max_size_required_for_baddbmm > (mem_free * 3.0 / 4.0): + return "max" + elif torch.backends.mps.is_available(): + return "max" + else: + return "balanced" diff --git a/invokeai/backend/util/build_line.py b/invokeai/backend/util/build_line.py new file mode 100644 index 00000000000..77cf98d8df6 --- /dev/null +++ b/invokeai/backend/util/build_line.py @@ -0,0 +1,6 @@ +from typing import Callable + + +def build_line(x1: float, y1: float, x2: float, y2: float) -> Callable[[float], float]: + """Build a linear function given two points on the line (x1, y1) and (x2, y2).""" + return lambda x: (y2 - y1) / (x2 - x1) * (x - x1) + y1 diff --git a/invokeai/backend/util/calc_tensor_size.py b/invokeai/backend/util/calc_tensor_size.py new file mode 100644 index 00000000000..70b99cd8849 --- /dev/null +++ b/invokeai/backend/util/calc_tensor_size.py @@ -0,0 +1,11 @@ +import torch + + +def calc_tensor_size(t: torch.Tensor) -> int: + """Calculate the size of a tensor in bytes.""" + return t.nelement() * t.element_size() + + +def calc_tensors_size(tensors: list[torch.Tensor | None]) -> int: + """Calculate the size of a list of tensors in bytes.""" + return sum(calc_tensor_size(t) for t in tensors if t is not None) diff --git a/invokeai/backend/util/catch_sigint.py b/invokeai/backend/util/catch_sigint.py new file mode 100644 index 00000000000..b9735d94f96 --- /dev/null +++ b/invokeai/backend/util/catch_sigint.py @@ -0,0 +1,29 @@ +""" +This module defines a context manager `catch_sigint()` which temporarily replaces +the sigINT handler defined by the ASGI in order to allow the user to ^C the application +and shut it down immediately. This was implemented in order to allow the user to interrupt +slow model hashing during startup. + +Use like this: + + from invokeai.backend.util.catch_sigint import catch_sigint + with catch_sigint(): + run_some_hard_to_interrupt_process() +""" + +import signal +from contextlib import contextmanager +from typing import Generator + + +def sigint_handler(signum, frame): # type: ignore + signal.signal(signal.SIGINT, signal.SIG_DFL) + signal.raise_signal(signal.SIGINT) + + +@contextmanager +def catch_sigint() -> Generator[None, None, None]: + original_handler = signal.getsignal(signal.SIGINT) + signal.signal(signal.SIGINT, sigint_handler) + yield + signal.signal(signal.SIGINT, original_handler) diff --git a/invokeai/backend/util/devices.py b/invokeai/backend/util/devices.py new file mode 100644 index 00000000000..359ce45dc4f --- /dev/null +++ b/invokeai/backend/util/devices.py @@ -0,0 +1,153 @@ +from typing import Dict, Literal, Optional, Union + +import torch +from deprecated import deprecated + +from invokeai.app.services.config.config_default import get_config + +# legacy APIs +TorchPrecisionNames = Literal["float32", "float16", "bfloat16"] +CPU_DEVICE = torch.device("cpu") +CUDA_DEVICE = torch.device("cuda") +MPS_DEVICE = torch.device("mps") + + +@deprecated("Use TorchDevice.choose_torch_dtype() instead.") # type: ignore +def choose_precision(device: torch.device) -> TorchPrecisionNames: + """Return the string representation of the recommended torch device.""" + torch_dtype = TorchDevice.choose_torch_dtype(device) + return PRECISION_TO_NAME[torch_dtype] + + +@deprecated("Use TorchDevice.choose_torch_device() instead.") # type: ignore +def choose_torch_device() -> torch.device: + """Return the torch.device to use for accelerated inference.""" + return TorchDevice.choose_torch_device() + + +@deprecated("Use TorchDevice.choose_torch_dtype() instead.") # type: ignore +def torch_dtype(device: torch.device) -> torch.dtype: + """Return the torch precision for the recommended torch device.""" + return TorchDevice.choose_torch_dtype(device) + + +NAME_TO_PRECISION: Dict[TorchPrecisionNames, torch.dtype] = { + "float32": torch.float32, + "float16": torch.float16, + "bfloat16": torch.bfloat16, +} +PRECISION_TO_NAME: Dict[torch.dtype, TorchPrecisionNames] = {v: k for k, v in NAME_TO_PRECISION.items()} + + +class TorchDevice: + """Abstraction layer for torch devices.""" + + CPU_DEVICE = torch.device("cpu") + CUDA_DEVICE = torch.device("cuda") + MPS_DEVICE = torch.device("mps") + + @classmethod + def choose_torch_device(cls) -> torch.device: + """Return the torch.device to use for accelerated inference.""" + app_config = get_config() + if app_config.device != "auto": + device = torch.device(app_config.device) + elif torch.cuda.is_available(): + device = CUDA_DEVICE + elif torch.backends.mps.is_available(): + device = MPS_DEVICE + else: + device = CPU_DEVICE + return cls.normalize(device) + + @classmethod + def choose_torch_dtype(cls, device: Optional[torch.device] = None) -> torch.dtype: + """Return the precision to use for accelerated inference.""" + device = device or cls.choose_torch_device() + config = get_config() + if device.type == "cuda" and torch.cuda.is_available(): + device_name = torch.cuda.get_device_name(device) + if "GeForce GTX 1660" in device_name or "GeForce GTX 1650" in device_name: + # These GPUs have limited support for float16 + return cls._to_dtype("float32") + elif config.precision == "auto": + # Default to float16 for CUDA devices + return cls._to_dtype("float16") + else: + # Use the user-defined precision + return cls._to_dtype(config.precision) + + elif device.type == "mps" and torch.backends.mps.is_available(): + if config.precision == "auto": + # Default to float16 for MPS devices + return cls._to_dtype("float16") + else: + # Use the user-defined precision + return cls._to_dtype(config.precision) + # CPU / safe fallback + return cls._to_dtype("float32") + + @classmethod + def get_torch_device_name(cls) -> str: + """Return the device name for the current torch device.""" + device = cls.choose_torch_device() + return torch.cuda.get_device_name(device) if device.type == "cuda" else device.type.upper() + + @classmethod + def normalize(cls, device: Union[str, torch.device]) -> torch.device: + """Add the device index to CUDA devices.""" + device = torch.device(device) + if device.index is None and device.type == "cuda" and torch.cuda.is_available(): + device = torch.device(device.type, torch.cuda.current_device()) + return device + + @classmethod + def empty_cache(cls) -> None: + """Clear the GPU device cache.""" + if torch.backends.mps.is_available(): + torch.mps.empty_cache() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + @classmethod + def _to_dtype(cls, precision_name: TorchPrecisionNames) -> torch.dtype: + return NAME_TO_PRECISION[precision_name] + + @classmethod + def choose_bfloat16_safe_dtype(cls, device: Optional[torch.device] = None) -> torch.dtype: + """Return bfloat16 if supported on the device, else fallback to float16/float32. + + This is useful for models that require bfloat16 precision (e.g., Z-Image, Flux) + but need to run on hardware that may not support bfloat16. + + Args: + device: The target device. If None, uses choose_torch_device(). + + Returns: + torch.bfloat16 if supported, torch.float16 for CUDA without bfloat16 support, + or torch.float32 for CPU/MPS. + """ + device = device or cls.choose_torch_device() + try: + # Test if bfloat16 is supported on this device + torch.tensor([1.0], dtype=torch.bfloat16, device=device) + return torch.bfloat16 + except TypeError: + # bfloat16 not supported - fallback based on device type + if device.type == "cuda": + return torch.float16 + return torch.float32 + + @classmethod + def choose_anima_inference_dtype(cls, device: Optional[torch.device] = None) -> torch.dtype: + """Choose the inference dtype for Anima models, honoring config.precision. + + When precision is 'auto', delegates to choose_bfloat16_safe_dtype (current + behavior). When precision is set to a specific value (float16, bfloat16, + float32), returns that dtype directly without hardware probing. + """ + device = device or cls.choose_torch_device() + config = get_config() + if config.precision == "auto": + return cls.choose_bfloat16_safe_dtype(device) + return NAME_TO_PRECISION[config.precision] diff --git a/invokeai/backend/util/gallery_maintenance.py b/invokeai/backend/util/gallery_maintenance.py new file mode 100644 index 00000000000..e7d3432121f --- /dev/null +++ b/invokeai/backend/util/gallery_maintenance.py @@ -0,0 +1,577 @@ +# pylint: disable=line-too-long +# pylint: disable=broad-exception-caught +# pylint: disable=missing-function-docstring +"""Script to peform db maintenance and outputs directory management.""" + +import argparse +import datetime +import enum +import glob +import locale +import os +import shutil +import sqlite3 +from pathlib import Path + +import PIL +import PIL.ImageOps +import PIL.PngImagePlugin +import yaml + + +class ConfigMapper: + """Configuration loader.""" + + def __init__(self): # noqa D107 + pass + + TIMESTAMP_STRING = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") + + INVOKE_DIRNAME = "invokeai" + YAML_FILENAME = "invokeai.yaml" + DATABASE_FILENAME = "invokeai.db" + + DEFAULT_OUTDIR = "outputs" + DEFAULT_DB_DIR = "databases" + + database_path = None + database_backup_dir = None + outputs_path = None + archive_path = None + thumbnails_path = None + thumbnails_archive_path = None + + def load(self): + """Read paths from yaml config and validate.""" + root = "." + + if not self.__load_from_root_config(os.path.abspath(root)): + return False + + return True + + def __load_from_root_config(self, invoke_root): + """Validate a yaml path exists, confirm the user wants to use it and load config.""" + yaml_path = os.path.join(invoke_root, self.YAML_FILENAME) + if not os.path.exists(yaml_path): + print(f"Unable to find invokeai.yaml at {yaml_path}!") + return False + if os.path.exists(yaml_path): + db_dir, outdir = self.__load_paths_from_yaml_file(yaml_path) + + if db_dir is None: + db_dir = self.DEFAULT_DB_DIR + print(f"The invokeai.yaml file was found but is missing the db_dir setting! Defaulting to {db_dir}") + if outdir is None: + outdir = self.DEFAULT_OUTDIR + print(f"The invokeai.yaml file was found but is missing the outdir setting! Defaulting to {outdir}") + + if os.path.isabs(db_dir): + self.database_path = os.path.join(db_dir, self.DATABASE_FILENAME) + else: + self.database_path = os.path.join(invoke_root, db_dir, self.DATABASE_FILENAME) + + self.database_backup_dir = os.path.join(os.path.dirname(self.database_path), "backup") + + if os.path.isabs(outdir): + self.outputs_path = os.path.join(outdir, "images") + self.archive_path = os.path.join(outdir, "images-archive") + else: + self.outputs_path = os.path.join(invoke_root, outdir, "images") + self.archive_path = os.path.join(invoke_root, outdir, "images-archive") + + self.thumbnails_path = os.path.join(self.outputs_path, "thumbnails") + self.thumbnails_archive_path = os.path.join(self.archive_path, "thumbnails") + + db_exists = os.path.exists(self.database_path) + outdir_exists = os.path.exists(self.outputs_path) + + text = f"Found {self.YAML_FILENAME} file at {yaml_path}:" + text += f"\n Database : {self.database_path} - {'Exists!' if db_exists else 'Not Found!'}" + text += f"\n Outputs : {self.outputs_path}- {'Exists!' if outdir_exists else 'Not Found!'}" + print(text) + + if db_exists and outdir_exists: + return True + else: + print( + "\nOne or more paths specified in invoke.yaml do not exist. Please inspect/correct the configuration and ensure the script is run in the developer console mode (option 8) from an Invoke AI root directory." + ) + return False + else: + print( + f"Auto-discovery of configuration failed! Could not find ({yaml_path})!\n\nPlease ensure the script is run in the developer console mode (option 8) from an Invoke AI root directory." + ) + return False + + def __load_paths_from_yaml_file(self, yaml_path): + """Load an Invoke AI yaml file and get the database and outputs paths.""" + try: + with open(yaml_path, "rt", encoding=locale.getpreferredencoding()) as file: + yamlinfo = yaml.safe_load(file) + db_dir = yamlinfo.get("InvokeAI", {}).get("Paths", {}).get("db_dir", None) + outdir = yamlinfo.get("InvokeAI", {}).get("Paths", {}).get("outdir", None) + return db_dir, outdir + except Exception: + print(f"Failed to load paths from yaml file! {yaml_path}!") + return None, None + + +class MaintenanceStats: + """DTO for tracking work progress.""" + + def __init__(self): # noqa D107 + pass + + time_start = datetime.datetime.utcnow() + count_orphaned_db_entries_cleaned = 0 + count_orphaned_disk_files_cleaned = 0 + count_orphaned_thumbnails_cleaned = 0 + count_thumbnails_regenerated = 0 + count_errors = 0 + + @staticmethod + def get_elapsed_time_string(): + """Get a friendly time string for the time elapsed since processing start.""" + time_now = datetime.datetime.utcnow() + total_seconds = (time_now - MaintenanceStats.time_start).total_seconds() + hours = int((total_seconds) / 3600) + minutes = int(((total_seconds) % 3600) / 60) + seconds = total_seconds % 60 + out_str = f"{hours} hour(s) -" if hours > 0 else "" + out_str += f"{minutes} minute(s) -" if minutes > 0 else "" + out_str += f"{seconds:.2f} second(s)" + return out_str + + +class DatabaseMapper: + """Class to abstract database functionality.""" + + def __init__(self, database_path, database_backup_dir): # noqa D107 + self.database_path = database_path + self.database_backup_dir = database_backup_dir + self.connection = None + self.cursor = None + + def backup(self, timestamp_string): + """Take a backup of the database.""" + if not os.path.exists(self.database_backup_dir): + print(f"Database backup directory {self.database_backup_dir} does not exist -> creating...", end="") + os.makedirs(self.database_backup_dir) + print("Done!") + database_backup_path = os.path.join(self.database_backup_dir, f"backup-{timestamp_string}-invokeai.db") + print(f"Making DB Backup at {database_backup_path}...", end="") + shutil.copy2(self.database_path, database_backup_path) + print("Done!") + + def connect(self): + """Open connection to the database.""" + self.connection = sqlite3.connect(self.database_path) + self.cursor = self.connection.cursor() + + def get_all_image_files(self): + """Get the full list of image file names from the database.""" + sql_get_image_by_name = "SELECT image_name FROM images" + self.cursor.execute(sql_get_image_by_name) + rows = self.cursor.fetchall() + db_files = [] + for row in rows: + db_files.append(row[0]) + return db_files + + def remove_image_file_record(self, filename: str): + """Remove an image file reference from the database by filename.""" + sanitized_filename = str.replace(filename, "'", "''") # prevent injection + sql_command = f"DELETE FROM images WHERE image_name='{sanitized_filename}'" + self.cursor.execute(sql_command) + self.connection.commit() + + def does_image_exist(self, image_filename): + """Check database if a image name already exists and return a boolean.""" + sanitized_filename = str.replace(image_filename, "'", "''") # prevent injection + sql_get_image_by_name = f"SELECT image_name FROM images WHERE image_name='{sanitized_filename}'" + self.cursor.execute(sql_get_image_by_name) + rows = self.cursor.fetchall() + return True if len(rows) > 0 else False + + def disconnect(self): + """Disconnect from the db, cleaning up connections and cursors.""" + if self.cursor is not None: + self.cursor.close() + if self.connection is not None: + self.connection.close() + + +class PhysicalFileMapper: + """Containing class for script functionality.""" + + def __init__(self, outputs_path, thumbnails_path, archive_path, thumbnails_archive_path): # noqa D107 + self.outputs_path = outputs_path + self.archive_path = archive_path + self.thumbnails_path = thumbnails_path + self.thumbnails_archive_path = thumbnails_archive_path + + def create_archive_directories(self): + """Create the directory for archiving orphaned image files.""" + if not os.path.exists(self.archive_path): + print(f"Image archive directory ({self.archive_path}) does not exist -> creating...", end="") + os.makedirs(self.archive_path) + print("Created!") + if not os.path.exists(self.thumbnails_archive_path): + print( + f"Image thumbnails archive directory ({self.thumbnails_archive_path}) does not exist -> creating...", + end="", + ) + os.makedirs(self.thumbnails_archive_path) + print("Created!") + + def get_image_path_for_image_name(self, image_filename): # noqa D102 + return os.path.join(self.outputs_path, image_filename) + + def image_file_exists(self, image_filename): # noqa D102 + return os.path.exists(self.get_image_path_for_image_name(image_filename)) + + def get_thumbnail_path_for_image(self, image_filename): # noqa D102 + return os.path.join(self.thumbnails_path, os.path.splitext(image_filename)[0]) + ".webp" + + def get_image_name_from_thumbnail_path(self, thumbnail_path): # noqa D102 + return os.path.splitext(os.path.basename(thumbnail_path))[0] + ".png" + + def thumbnail_exists_for_filename(self, image_filename): # noqa D102 + return os.path.exists(self.get_thumbnail_path_for_image(image_filename)) + + def archive_image(self, image_filename): # noqa D102 + if self.image_file_exists(image_filename): + image_path = self.get_image_path_for_image_name(image_filename) + shutil.move(image_path, self.archive_path) + + def archive_thumbnail_by_image_filename(self, image_filename): # noqa D102 + if self.thumbnail_exists_for_filename(image_filename): + thumbnail_path = self.get_thumbnail_path_for_image(image_filename) + shutil.move(thumbnail_path, self.thumbnails_archive_path) + + def get_all_png_filenames_in_directory(self, directory_path): # noqa D102 + filepaths = glob.glob(directory_path + "/*.png", recursive=False) + filenames = [] + for filepath in filepaths: + filenames.append(os.path.basename(filepath)) + return filenames + + def get_all_thumbnails_with_full_path(self, thumbnails_directory): # noqa D102 + return glob.glob(thumbnails_directory + "/*.webp", recursive=False) + + def generate_thumbnail_for_image_name(self, image_filename): # noqa D102 + # create thumbnail + file_path = self.get_image_path_for_image_name(image_filename) + thumb_path = self.get_thumbnail_path_for_image(image_filename) + thumb_size = 256, 256 + with PIL.Image.open(file_path) as source_image: + source_image.thumbnail(thumb_size) + source_image.save(thumb_path, "webp") + + +class MaintenanceOperation(str, enum.Enum): + """Enum class for operations.""" + + Ask = "ask" + CleanOrphanedDbEntries = "clean" + CleanOrphanedDiskFiles = "archive" + ReGenerateThumbnails = "thumbnails" + All = "all" + + +class InvokeAIDatabaseMaintenanceApp: + """Main processor class for the application.""" + + _operation: MaintenanceOperation + _headless: bool = False + __stats: MaintenanceStats = MaintenanceStats() + + def __init__(self, operation: MaintenanceOperation = MaintenanceOperation.Ask): + """Initialize maintenance app.""" + self._operation = MaintenanceOperation(operation) + self._headless = operation != MaintenanceOperation.Ask + + def ask_for_operation(self) -> MaintenanceOperation: + """Ask user to choose the operation to perform.""" + while True: + print() + print("It is recommennded to run these operations as ordered below to avoid additional") + print("work being performed that will be discarded in a subsequent step.") + print() + print("Select maintenance operation:") + print() + print("1) Clean Orphaned Database Image Entries") + print(" Cleans entries in the database where the matching file was removed from") + print(" the outputs directory.") + print("2) Archive Orphaned Image Files") + print(" Files found in the outputs directory without an entry in the database are") + print(" moved to an archive directory.") + print("3) Re-Generate Missing Thumbnail Files") + print(" For files found in the outputs directory, re-generate a thumbnail if it") + print(" not found in the thumbnails directory.") + print() + print("(CTRL-C to quit)") + + try: + input_option = int(input("Specify desired operation number (1-3): ")) + + operations = [ + MaintenanceOperation.CleanOrphanedDbEntries, + MaintenanceOperation.CleanOrphanedDiskFiles, + MaintenanceOperation.ReGenerateThumbnails, + ] + return operations[input_option - 1] + except (IndexError, ValueError): + print("\nInvalid selection!") + + def ask_to_continue(self) -> bool: + """Ask user whether they want to continue with the operation.""" + while True: + input_choice = input("Do you wish to continue? (Y or N)? ") + if str.lower(input_choice) == "y": + return True + if str.lower(input_choice) == "n": + return False + + def clean_orphaned_db_entries( + self, config: ConfigMapper, file_mapper: PhysicalFileMapper, db_mapper: DatabaseMapper + ): + """Clean dangling database entries that no longer point to a file in outputs.""" + if self._headless: + print(f"Removing database references to images that no longer exist in {config.outputs_path}...") + else: + print() + print("===============================================================================") + print("= Clean Orphaned Database Entries") + print() + print("Perform this operation if you have removed files from the outputs/images") + print("directory but the database was never updated. You may see this as empty imaages") + print("in the app gallery, or images that only show an enlarged version of the") + print("thumbnail.") + print() + print(f"Database File Path : {config.database_path}") + print(f"Database backup will be taken at : {config.database_backup_dir}") + print(f"Outputs/Images Directory : {config.outputs_path}") + print(f"Outputs/Images Archive Directory : {config.archive_path}") + + print("\nNotes about this operation:") + print("- This operation will find database image file entries that do not exist in the") + print(" outputs/images dir and remove those entries from the database.") + print("- This operation will target all image types including intermediate files.") + print("- If a thumbnail still exists in outputs/images/thumbnails matching the") + print(" orphaned entry, it will be moved to the archive directory.") + print() + + if not self.ask_to_continue(): + raise KeyboardInterrupt + + file_mapper.create_archive_directories() + db_mapper.backup(config.TIMESTAMP_STRING) + db_mapper.connect() + db_files = db_mapper.get_all_image_files() + for db_file in db_files: + try: + if not file_mapper.image_file_exists(db_file): + print(f"Found orphaned image db entry {db_file}. Cleaning ...", end="") + db_mapper.remove_image_file_record(db_file) + print("Cleaned!") + if file_mapper.thumbnail_exists_for_filename(db_file): + print("A thumbnail was found, archiving ...", end="") + file_mapper.archive_thumbnail_by_image_filename(db_file) + print("Archived!") + self.__stats.count_orphaned_db_entries_cleaned += 1 + except Exception as ex: + print("An error occurred cleaning db entry, error was:") + print(ex) + self.__stats.count_errors += 1 + + def clean_orphaned_disk_files( + self, config: ConfigMapper, file_mapper: PhysicalFileMapper, db_mapper: DatabaseMapper + ): + """Archive image files that no longer have entries in the database.""" + if self._headless: + print(f"Archiving orphaned image files to {config.archive_path}...") + else: + print() + print("===============================================================================") + print("= Clean Orphaned Disk Files") + print() + print("Perform this operation if you have files that were copied into the outputs") + print("directory which are not referenced by the database. This can happen if you") + print("upgraded to a version with a fresh database, but re-used the outputs directory") + print("and now new images are mixed with the files not in the db. The script will") + print("archive these files so you can choose to delete them or re-import using the") + print("official import script.") + print() + print(f"Database File Path : {config.database_path}") + print(f"Database backup will be taken at : {config.database_backup_dir}") + print(f"Outputs/Images Directory : {config.outputs_path}") + print(f"Outputs/Images Archive Directory : {config.archive_path}") + + print("\nNotes about this operation:") + print("- This operation will find image files not referenced by the database and move to an") + print(" archive directory.") + print("- This operation will target all image types including intermediate references.") + print("- The matching thumbnail will also be archived.") + print("- Any remaining orphaned thumbnails will also be archived.") + + if not self.ask_to_continue(): + raise KeyboardInterrupt + + print() + + file_mapper.create_archive_directories() + db_mapper.backup(config.TIMESTAMP_STRING) + db_mapper.connect() + phys_files = file_mapper.get_all_png_filenames_in_directory(config.outputs_path) + for phys_file in phys_files: + try: + if not db_mapper.does_image_exist(phys_file): + print(f"Found orphaned file {phys_file}, archiving...", end="") + file_mapper.archive_image(phys_file) + print("Archived!") + if file_mapper.thumbnail_exists_for_filename(phys_file): + print("Related thumbnail exists, archiving...", end="") + file_mapper.archive_thumbnail_by_image_filename(phys_file) + print("Archived!") + else: + print("No matching thumbnail existed to be cleaned.") + self.__stats.count_orphaned_disk_files_cleaned += 1 + except Exception as ex: + print("Error found trying to archive file or thumbnail, error was:") + print(ex) + self.__stats.count_errors += 1 + + thumb_filepaths = file_mapper.get_all_thumbnails_with_full_path(config.thumbnails_path) + # archive any remaining orphaned thumbnails + for thumb_filepath in thumb_filepaths: + try: + thumb_src_image_name = file_mapper.get_image_name_from_thumbnail_path(thumb_filepath) + if not file_mapper.image_file_exists(thumb_src_image_name): + print(f"Found orphaned thumbnail {thumb_filepath}, archiving...", end="") + file_mapper.archive_thumbnail_by_image_filename(thumb_src_image_name) + print("Archived!") + self.__stats.count_orphaned_thumbnails_cleaned += 1 + except Exception as ex: + print("Error found trying to archive thumbnail, error was:") + print(ex) + self.__stats.count_errors += 1 + + def regenerate_thumbnails(self, config: ConfigMapper, file_mapper: PhysicalFileMapper, *args): + """Create missing thumbnails for any valid general images both in the db and on disk.""" + if self._headless: + print("Regenerating missing image thumbnails...") + else: + print() + print("===============================================================================") + print("= Regenerate Thumbnails") + print() + print("This operation will find files that have no matching thumbnail on disk") + print("and regenerate those thumbnail files.") + print("NOTE: It is STRONGLY recommended that the user first clean/archive orphaned") + print(" disk files from the previous menu to avoid wasting time regenerating") + print(" thumbnails for orphaned files.") + + print() + print(f"Outputs/Images Directory : {config.outputs_path}") + print(f"Outputs/Images Directory : {config.thumbnails_path}") + + print("\nNotes about this operation:") + print("- This operation will find image files both referenced in the db and on disk") + print(" that do not have a matching thumbnail on disk and re-generate the thumbnail") + print(" file.") + + if not self.ask_to_continue(): + raise KeyboardInterrupt + + print() + + phys_files = file_mapper.get_all_png_filenames_in_directory(config.outputs_path) + for phys_file in phys_files: + try: + if not file_mapper.thumbnail_exists_for_filename(phys_file): + print(f"Found file without thumbnail {phys_file}...Regenerating Thumbnail...", end="") + file_mapper.generate_thumbnail_for_image_name(phys_file) + print("Done!") + self.__stats.count_thumbnails_regenerated += 1 + except Exception as ex: + print("Error found trying to regenerate thumbnail, error was:") + print(ex) + self.__stats.count_errors += 1 + + def main(self): # noqa D107 + print("\n===============================================================================") + print("Database and outputs Maintenance for Invoke AI 3.0.0 +") + print("===============================================================================\n") + + config_mapper = ConfigMapper() + if not config_mapper.load(): + print("\nInvalid configuration...exiting.\n") + return + + file_mapper = PhysicalFileMapper( + config_mapper.outputs_path, + config_mapper.thumbnails_path, + config_mapper.archive_path, + config_mapper.thumbnails_archive_path, + ) + db_mapper = DatabaseMapper(config_mapper.database_path, config_mapper.database_backup_dir) + + op = self._operation + operations_to_perform = [] + + if op == MaintenanceOperation.Ask: + op = self.ask_for_operation() + + if op in [MaintenanceOperation.CleanOrphanedDbEntries, MaintenanceOperation.All]: + operations_to_perform.append(self.clean_orphaned_db_entries) + if op in [MaintenanceOperation.CleanOrphanedDiskFiles, MaintenanceOperation.All]: + operations_to_perform.append(self.clean_orphaned_disk_files) + if op in [MaintenanceOperation.ReGenerateThumbnails, MaintenanceOperation.All]: + operations_to_perform.append(self.regenerate_thumbnails) + + for operation in operations_to_perform: + operation(config_mapper, file_mapper, db_mapper) + + print("\n===============================================================================") + print(f"= Maintenance Complete - Elapsed Time: {MaintenanceStats.get_elapsed_time_string()}") + print() + print(f"Orphaned db entries cleaned : {self.__stats.count_orphaned_db_entries_cleaned}") + print(f"Orphaned disk files archived : {self.__stats.count_orphaned_disk_files_cleaned}") + print(f"Orphaned thumbnail files archived : {self.__stats.count_orphaned_thumbnails_cleaned}") + print(f"Thumbnails regenerated : {self.__stats.count_thumbnails_regenerated}") + print(f"Errors during operation : {self.__stats.count_errors}") + + print() + + +def main(): # noqa D107 + parser = argparse.ArgumentParser( + description="InvokeAI image database maintenance utility", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""Operations: + ask Choose operation from a menu [default] + all Run all maintenance operations + clean Clean database of dangling entries + archive Archive orphaned image files + thumbnails Regenerate missing image thumbnails +""", + ) + parser.add_argument("--root", default=".", type=Path, help="InvokeAI root directory") + parser.add_argument( + "--operation", default="ask", choices=[x.value for x in MaintenanceOperation], help="Operation to perform." + ) + args = parser.parse_args() + try: + os.chdir(args.root) + app = InvokeAIDatabaseMaintenanceApp(args.operation) + app.main() + except KeyboardInterrupt: + print("\n\nUser cancelled execution.") + except FileNotFoundError: + print(f"Invalid root directory '{args.root}'.") + + +if __name__ == "__main__": + main() diff --git a/invokeai/backend/util/hotfixes.py b/invokeai/backend/util/hotfixes.py new file mode 100644 index 00000000000..57b07f9d267 --- /dev/null +++ b/invokeai/backend/util/hotfixes.py @@ -0,0 +1,843 @@ +from typing import Any, Dict, List, Optional, Tuple, Union + +import diffusers +import torch +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.loaders.single_file_model import FromOriginalModelMixin +from diffusers.models.attention_processor import AttentionProcessor, AttnProcessor +from diffusers.models.controlnets.controlnet import ControlNetConditioningEmbedding, ControlNetOutput, zero_module +from diffusers.models.embeddings import ( + TextImageProjection, + TextImageTimeEmbedding, + TextTimeEmbedding, + TimestepEmbedding, + Timesteps, +) +from diffusers.models.modeling_utils import ModelMixin +from diffusers.models.unets.unet_2d_blocks import ( + CrossAttnDownBlock2D, + DownBlock2D, + UNetMidBlock2DCrossAttn, + get_down_block, +) +from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel +from torch import nn + +from invokeai.backend.model_manager.taxonomy import BaseModelType, SchedulerPredictionType +from invokeai.backend.util.logging import InvokeAILogger + +# TODO: create PR to diffusers +# Modified ControlNetModel with encoder_attention_mask argument added + + +logger = InvokeAILogger.get_logger(__name__) + + +# NOTE(ryand): I'm not the origina author of this code, but for future reference, it appears that this class was copied +# from diffusers in order to add support for the encoder_attention_mask argument. +class ControlNetModel(ModelMixin, ConfigMixin, FromOriginalModelMixin): + """ + A ControlNet model. + + Args: + in_channels (`int`, defaults to 4): + The number of channels in the input sample. + flip_sin_to_cos (`bool`, defaults to `True`): + Whether to flip the sin to cos in the time embedding. + freq_shift (`int`, defaults to 0): + The frequency shift to apply to the time embedding. + down_block_types (`tuple[str]`, defaults to `("CrossAttnDownBlock2D", "CrossAttnDownBlock2D", \ + "CrossAttnDownBlock2D", "DownBlock2D")`): + The tuple of downsample blocks to use. + only_cross_attention (`Union[bool, Tuple[bool]]`, defaults to `False`): + block_out_channels (`tuple[int]`, defaults to `(320, 640, 1280, 1280)`): + The tuple of output channels for each block. + layers_per_block (`int`, defaults to 2): + The number of layers per block. + downsample_padding (`int`, defaults to 1): + The padding to use for the downsampling convolution. + mid_block_scale_factor (`float`, defaults to 1): + The scale factor to use for the mid block. + act_fn (`str`, defaults to "silu"): + The activation function to use. + norm_num_groups (`int`, *optional*, defaults to 32): + The number of groups to use for the normalization. If None, normalization and activation layers is skipped + in post-processing. + norm_eps (`float`, defaults to 1e-5): + The epsilon to use for the normalization. + cross_attention_dim (`int`, defaults to 1280): + The dimension of the cross attention features. + transformer_layers_per_block (`int` or `Tuple[int]`, *optional*, defaults to 1): + The number of transformer blocks of type [`~models.attention.BasicTransformerBlock`]. Only relevant for + [`~models.unet_2d_blocks.CrossAttnDownBlock2D`], [`~models.unet_2d_blocks.CrossAttnUpBlock2D`], + [`~models.unet_2d_blocks.UNetMidBlock2DCrossAttn`]. + encoder_hid_dim (`int`, *optional*, defaults to None): + If `encoder_hid_dim_type` is defined, `encoder_hidden_states` will be projected from `encoder_hid_dim` + dimension to `cross_attention_dim`. + encoder_hid_dim_type (`str`, *optional*, defaults to `None`): + If given, the `encoder_hidden_states` and potentially other embeddings are down-projected to text + embeddings of dimension `cross_attention` according to `encoder_hid_dim_type`. + attention_head_dim (`Union[int, Tuple[int]]`, defaults to 8): + The dimension of the attention heads. + use_linear_projection (`bool`, defaults to `False`): + class_embed_type (`str`, *optional*, defaults to `None`): + The type of class embedding to use which is ultimately summed with the time embeddings. Choose from None, + `"timestep"`, `"identity"`, `"projection"`, or `"simple_projection"`. + addition_embed_type (`str`, *optional*, defaults to `None`): + Configures an optional embedding which will be summed with the time embeddings. Choose from `None` or + "text". "text" will use the `TextTimeEmbedding` layer. + num_class_embeds (`int`, *optional*, defaults to 0): + Input dimension of the learnable embedding matrix to be projected to `time_embed_dim`, when performing + class conditioning with `class_embed_type` equal to `None`. + upcast_attention (`bool`, defaults to `False`): + resnet_time_scale_shift (`str`, defaults to `"default"`): + Time scale shift config for ResNet blocks (see `ResnetBlock2D`). Choose from `default` or `scale_shift`. + projection_class_embeddings_input_dim (`int`, *optional*, defaults to `None`): + The dimension of the `class_labels` input when `class_embed_type="projection"`. Required when + `class_embed_type="projection"`. + controlnet_conditioning_channel_order (`str`, defaults to `"rgb"`): + The channel order of conditional image. Will convert to `rgb` if it's `bgr`. + conditioning_embedding_out_channels (`tuple[int]`, *optional*, defaults to `(16, 32, 96, 256)`): + The tuple of output channel for each block in the `conditioning_embedding` layer. + global_pool_conditions (`bool`, defaults to `False`): + """ + + _supports_gradient_checkpointing = True + + @register_to_config + def __init__( + self, + in_channels: int = 4, + conditioning_channels: int = 3, + flip_sin_to_cos: bool = True, + freq_shift: int = 0, + down_block_types: Tuple[str] = ( + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "DownBlock2D", + ), + only_cross_attention: Union[bool, Tuple[bool]] = False, + block_out_channels: Tuple[int, ...] = (320, 640, 1280, 1280), + layers_per_block: int = 2, + downsample_padding: int = 1, + mid_block_scale_factor: float = 1, + act_fn: str = "silu", + norm_num_groups: Optional[int] = 32, + norm_eps: float = 1e-5, + cross_attention_dim: int = 1280, + transformer_layers_per_block: Union[int, Tuple[int]] = 1, + encoder_hid_dim: Optional[int] = None, + encoder_hid_dim_type: Optional[str] = None, + attention_head_dim: Union[int, Tuple[int]] = 8, + num_attention_heads: Optional[Union[int, Tuple[int]]] = None, + use_linear_projection: bool = False, + class_embed_type: Optional[str] = None, + addition_embed_type: Optional[str] = None, + addition_time_embed_dim: Optional[int] = None, + num_class_embeds: Optional[int] = None, + upcast_attention: bool = False, + resnet_time_scale_shift: str = "default", + projection_class_embeddings_input_dim: Optional[int] = None, + controlnet_conditioning_channel_order: str = "rgb", + conditioning_embedding_out_channels: Optional[Tuple[int]] = (16, 32, 96, 256), + global_pool_conditions: bool = False, + addition_embed_type_num_heads=64, + ): + super().__init__() + + # If `num_attention_heads` is not defined (which is the case for most models) + # it will default to `attention_head_dim`. This looks weird upon first reading it and it is. + # The reason for this behavior is to correct for incorrectly named variables that were introduced + # when this library was created... + # The incorrect naming was only discovered much ... + # later in https://github.com/huggingface/diffusers/issues/2011#issuecomment-1547958131 + # Changing `attention_head_dim` to `num_attention_heads` for 40,000+ configurations is too backwards breaking + # which is why we correct for the naming here. + num_attention_heads = num_attention_heads or attention_head_dim + + # Check inputs + if len(block_out_channels) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `block_out_channels` as `down_block_types`. \ + `block_out_channels`: {block_out_channels}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(only_cross_attention, bool) and len(only_cross_attention) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `only_cross_attention` as `down_block_types`. \ + `only_cross_attention`: {only_cross_attention}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(num_attention_heads, int) and len(num_attention_heads) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `num_attention_heads` as `down_block_types`. \ + `num_attention_heads`: {num_attention_heads}. `down_block_types`: {down_block_types}." + ) + + if isinstance(transformer_layers_per_block, int): + transformer_layers_per_block = [transformer_layers_per_block] * len(down_block_types) + + # input + conv_in_kernel = 3 + conv_in_padding = (conv_in_kernel - 1) // 2 + self.conv_in = nn.Conv2d( + in_channels, block_out_channels[0], kernel_size=conv_in_kernel, padding=conv_in_padding + ) + + # time + time_embed_dim = block_out_channels[0] * 4 + self.time_proj = Timesteps(block_out_channels[0], flip_sin_to_cos, freq_shift) + timestep_input_dim = block_out_channels[0] + self.time_embedding = TimestepEmbedding( + timestep_input_dim, + time_embed_dim, + act_fn=act_fn, + ) + + if encoder_hid_dim_type is None and encoder_hid_dim is not None: + encoder_hid_dim_type = "text_proj" + self.register_to_config(encoder_hid_dim_type=encoder_hid_dim_type) + logger.info("encoder_hid_dim_type defaults to 'text_proj' as `encoder_hid_dim` is defined.") + + if encoder_hid_dim is None and encoder_hid_dim_type is not None: + raise ValueError( + f"`encoder_hid_dim` has to be defined when `encoder_hid_dim_type` is set to {encoder_hid_dim_type}." + ) + + if encoder_hid_dim_type == "text_proj": + self.encoder_hid_proj = nn.Linear(encoder_hid_dim, cross_attention_dim) + elif encoder_hid_dim_type == "text_image_proj": + # image_embed_dim DOESN'T have to be `cross_attention_dim`. To not clutter the __init__ too much + # they are set to `cross_attention_dim` here as this is exactly the required dimension ... + # for the currently only use + # case when `addition_embed_type == "text_image_proj"` (Kadinsky 2.1)` + self.encoder_hid_proj = TextImageProjection( + text_embed_dim=encoder_hid_dim, + image_embed_dim=cross_attention_dim, + cross_attention_dim=cross_attention_dim, + ) + + elif encoder_hid_dim_type is not None: + raise ValueError( + f"encoder_hid_dim_type: {encoder_hid_dim_type} must be None, 'text_proj' or 'text_image_proj'." + ) + else: + self.encoder_hid_proj = None + + # class embedding + if class_embed_type is None and num_class_embeds is not None: + self.class_embedding = nn.Embedding(num_class_embeds, time_embed_dim) + elif class_embed_type == "timestep": + self.class_embedding = TimestepEmbedding(timestep_input_dim, time_embed_dim) + elif class_embed_type == "identity": + self.class_embedding = nn.Identity(time_embed_dim, time_embed_dim) + elif class_embed_type == "projection": + if projection_class_embeddings_input_dim is None: + raise ValueError( + "`class_embed_type`: 'projection' requires `projection_class_embeddings_input_dim` be set" + ) + # The projection `class_embed_type` is the same as the timestep `class_embed_type` except + # 1. the `class_labels` inputs are not first converted to sinusoidal embeddings + # 2. it projects from an arbitrary input dimension. + # + # Note that `TimestepEmbedding` is quite general, being mainly linear layers and activations. + # When used for embedding actual timesteps, the timesteps are first converted to sinusoidal embeddings. + # As a result, `TimestepEmbedding` can be passed arbitrary vectors. + self.class_embedding = TimestepEmbedding(projection_class_embeddings_input_dim, time_embed_dim) + else: + self.class_embedding = None + + if addition_embed_type == "text": + if encoder_hid_dim is not None: + text_time_embedding_from_dim = encoder_hid_dim + else: + text_time_embedding_from_dim = cross_attention_dim + + self.add_embedding = TextTimeEmbedding( + text_time_embedding_from_dim, time_embed_dim, num_heads=addition_embed_type_num_heads + ) + elif addition_embed_type == "text_image": + # text_embed_dim and image_embed_dim DON'T have to be `cross_attention_dim`. + # To not clutter the __init__ too much + # they are set to `cross_attention_dim` here as this is exactly the required dimension... + # for the currently only use + # case when `addition_embed_type == "text_image"` (Kadinsky 2.1)` + self.add_embedding = TextImageTimeEmbedding( + text_embed_dim=cross_attention_dim, image_embed_dim=cross_attention_dim, time_embed_dim=time_embed_dim + ) + elif addition_embed_type == "text_time": + self.add_time_proj = Timesteps(addition_time_embed_dim, flip_sin_to_cos, freq_shift) + self.add_embedding = TimestepEmbedding(projection_class_embeddings_input_dim, time_embed_dim) + + elif addition_embed_type is not None: + raise ValueError(f"addition_embed_type: {addition_embed_type} must be None, 'text' or 'text_image'.") + + # control net conditioning embedding + self.controlnet_cond_embedding = ControlNetConditioningEmbedding( + conditioning_embedding_channels=block_out_channels[0], + block_out_channels=conditioning_embedding_out_channels, + conditioning_channels=conditioning_channels, + ) + + self.down_blocks = nn.ModuleList([]) + self.controlnet_down_blocks = nn.ModuleList([]) + + if isinstance(only_cross_attention, bool): + only_cross_attention = [only_cross_attention] * len(down_block_types) + + if isinstance(attention_head_dim, int): + attention_head_dim = (attention_head_dim,) * len(down_block_types) + + if isinstance(num_attention_heads, int): + num_attention_heads = (num_attention_heads,) * len(down_block_types) + + # down + output_channel = block_out_channels[0] + + controlnet_block = nn.Conv2d(output_channel, output_channel, kernel_size=1) + controlnet_block = zero_module(controlnet_block) + self.controlnet_down_blocks.append(controlnet_block) + + for i, down_block_type in enumerate(down_block_types): + input_channel = output_channel + output_channel = block_out_channels[i] + is_final_block = i == len(block_out_channels) - 1 + + down_block = get_down_block( + down_block_type, + num_layers=layers_per_block, + transformer_layers_per_block=transformer_layers_per_block[i], + in_channels=input_channel, + out_channels=output_channel, + temb_channels=time_embed_dim, + add_downsample=not is_final_block, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + cross_attention_dim=cross_attention_dim, + num_attention_heads=num_attention_heads[i], + attention_head_dim=attention_head_dim[i] if attention_head_dim[i] is not None else output_channel, + downsample_padding=downsample_padding, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention[i], + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + self.down_blocks.append(down_block) + + for _ in range(layers_per_block): + controlnet_block = nn.Conv2d(output_channel, output_channel, kernel_size=1) + controlnet_block = zero_module(controlnet_block) + self.controlnet_down_blocks.append(controlnet_block) + + if not is_final_block: + controlnet_block = nn.Conv2d(output_channel, output_channel, kernel_size=1) + controlnet_block = zero_module(controlnet_block) + self.controlnet_down_blocks.append(controlnet_block) + + # mid + mid_block_channel = block_out_channels[-1] + + controlnet_block = nn.Conv2d(mid_block_channel, mid_block_channel, kernel_size=1) + controlnet_block = zero_module(controlnet_block) + self.controlnet_mid_block = controlnet_block + + self.mid_block = UNetMidBlock2DCrossAttn( + transformer_layers_per_block=transformer_layers_per_block[-1], + in_channels=mid_block_channel, + temb_channels=time_embed_dim, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + output_scale_factor=mid_block_scale_factor, + resnet_time_scale_shift=resnet_time_scale_shift, + cross_attention_dim=cross_attention_dim, + num_attention_heads=num_attention_heads[-1], + resnet_groups=norm_num_groups, + use_linear_projection=use_linear_projection, + upcast_attention=upcast_attention, + ) + + @classmethod + def from_unet( + cls, + unet: UNet2DConditionModel, + controlnet_conditioning_channel_order: str = "rgb", + conditioning_embedding_out_channels: Optional[Tuple[int]] = (16, 32, 96, 256), + load_weights_from_unet: bool = True, + ): + r""" + Instantiate a [`ControlNetModel`] from [`UNet2DConditionModel`]. + + Parameters: + unet (`UNet2DConditionModel`): + The UNet model weights to copy to the [`ControlNetModel`]. All configuration options are also copied + where applicable. + """ + transformer_layers_per_block = ( + unet.config.transformer_layers_per_block if "transformer_layers_per_block" in unet.config else 1 + ) + encoder_hid_dim = unet.config.encoder_hid_dim if "encoder_hid_dim" in unet.config else None + encoder_hid_dim_type = unet.config.encoder_hid_dim_type if "encoder_hid_dim_type" in unet.config else None + addition_embed_type = unet.config.addition_embed_type if "addition_embed_type" in unet.config else None + addition_time_embed_dim = ( + unet.config.addition_time_embed_dim if "addition_time_embed_dim" in unet.config else None + ) + + controlnet = cls( + encoder_hid_dim=encoder_hid_dim, + encoder_hid_dim_type=encoder_hid_dim_type, + addition_embed_type=addition_embed_type, + addition_time_embed_dim=addition_time_embed_dim, + transformer_layers_per_block=transformer_layers_per_block, + in_channels=unet.config.in_channels, + flip_sin_to_cos=unet.config.flip_sin_to_cos, + freq_shift=unet.config.freq_shift, + down_block_types=unet.config.down_block_types, + only_cross_attention=unet.config.only_cross_attention, + block_out_channels=unet.config.block_out_channels, + layers_per_block=unet.config.layers_per_block, + downsample_padding=unet.config.downsample_padding, + mid_block_scale_factor=unet.config.mid_block_scale_factor, + act_fn=unet.config.act_fn, + norm_num_groups=unet.config.norm_num_groups, + norm_eps=unet.config.norm_eps, + cross_attention_dim=unet.config.cross_attention_dim, + attention_head_dim=unet.config.attention_head_dim, + num_attention_heads=unet.config.num_attention_heads, + use_linear_projection=unet.config.use_linear_projection, + class_embed_type=unet.config.class_embed_type, + num_class_embeds=unet.config.num_class_embeds, + upcast_attention=unet.config.base is BaseModelType.StableDiffusion2 + and unet.config.prediction_type is SchedulerPredictionType.VPrediction, + resnet_time_scale_shift=unet.config.resnet_time_scale_shift, + projection_class_embeddings_input_dim=unet.config.projection_class_embeddings_input_dim, + controlnet_conditioning_channel_order=controlnet_conditioning_channel_order, + conditioning_embedding_out_channels=conditioning_embedding_out_channels, + ) + + if load_weights_from_unet: + controlnet.conv_in.load_state_dict(unet.conv_in.state_dict()) + controlnet.time_proj.load_state_dict(unet.time_proj.state_dict()) + controlnet.time_embedding.load_state_dict(unet.time_embedding.state_dict()) + + if controlnet.class_embedding: + controlnet.class_embedding.load_state_dict(unet.class_embedding.state_dict()) + + controlnet.down_blocks.load_state_dict(unet.down_blocks.state_dict()) + controlnet.mid_block.load_state_dict(unet.mid_block.state_dict()) + + return controlnet + + @property + # Copied from diffusers.models.unet_2d_condition.UNet2DConditionModel.attn_processors + def attn_processors(self) -> Dict[str, AttentionProcessor]: + r""" + Returns: + `dict` of attention processors: A dictionary containing all attention processors used in the model with + indexed by its weight name. + """ + # set recursively + processors = {} + + def fn_recursive_add_processors(name: str, module: torch.nn.Module, processors: Dict[str, AttentionProcessor]): + if hasattr(module, "set_processor"): + processors[f"{name}.processor"] = module.processor + + for sub_name, child in module.named_children(): + fn_recursive_add_processors(f"{name}.{sub_name}", child, processors) + + return processors + + for name, module in self.named_children(): + fn_recursive_add_processors(name, module, processors) + + return processors + + # Copied from diffusers.models.unet_2d_condition.UNet2DConditionModel.set_attn_processor + def set_attn_processor(self, processor: Union[AttentionProcessor, Dict[str, AttentionProcessor]]): + r""" + Sets the attention processor to use to compute attention. + + Parameters: + processor (`dict` of `AttentionProcessor` or only `AttentionProcessor`): + The instantiated processor class or a dictionary of processor classes that will be set as the processor + for **all** `Attention` layers. + + If `processor` is a dict, the key needs to define the path to the corresponding cross attention + processor. This is strongly recommended when setting trainable attention processors. + + """ + count = len(self.attn_processors.keys()) + + if isinstance(processor, dict) and len(processor) != count: + raise ValueError( + f"A dict of processors was passed, but the number of processors {len(processor)} does not match the" + f" number of attention layers: {count}. Please make sure to pass {count} processor classes." + ) + + def fn_recursive_attn_processor(name: str, module: torch.nn.Module, processor): + if hasattr(module, "set_processor"): + if not isinstance(processor, dict): + module.set_processor(processor) + else: + module.set_processor(processor.pop(f"{name}.processor")) + + for sub_name, child in module.named_children(): + fn_recursive_attn_processor(f"{name}.{sub_name}", child, processor) + + for name, module in self.named_children(): + fn_recursive_attn_processor(name, module, processor) + + # Copied from diffusers.models.unet_2d_condition.UNet2DConditionModel.set_default_attn_processor + def set_default_attn_processor(self): + """ + Disables custom attention processors and sets the default attention implementation. + """ + self.set_attn_processor(AttnProcessor()) + + # Copied from diffusers.models.unet_2d_condition.UNet2DConditionModel.set_attention_slice + def set_attention_slice(self, slice_size): + r""" + Enable sliced attention computation. + + When this option is enabled, the attention module splits the input tensor in slices to compute attention in + several steps. This is useful for saving some memory in exchange for a small decrease in speed. + + Args: + slice_size (`str` or `int` or `list(int)`, *optional*, defaults to `"auto"`): + When `"auto"`, input to the attention heads is halved, so attention is computed in two steps. If + `"max"`, maximum amount of memory is saved by running only one slice at a time. If a number is + provided, uses as many slices as `attention_head_dim // slice_size`. In this case, `attention_head_dim` + must be a multiple of `slice_size`. + """ + sliceable_head_dims = [] + + def fn_recursive_retrieve_sliceable_dims(module: torch.nn.Module): + if hasattr(module, "set_attention_slice"): + sliceable_head_dims.append(module.sliceable_head_dim) + + for child in module.children(): + fn_recursive_retrieve_sliceable_dims(child) + + # retrieve number of attention layers + for module in self.children(): + fn_recursive_retrieve_sliceable_dims(module) + + num_sliceable_layers = len(sliceable_head_dims) + + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = [dim // 2 for dim in sliceable_head_dims] + elif slice_size == "max": + # make smallest slice possible + slice_size = num_sliceable_layers * [1] + + slice_size = num_sliceable_layers * [slice_size] if not isinstance(slice_size, list) else slice_size + + if len(slice_size) != len(sliceable_head_dims): + raise ValueError( + f"You have provided {len(slice_size)}, but {self.config} has {len(sliceable_head_dims)} different" + f" attention layers. Make sure to match `len(slice_size)` to be {len(sliceable_head_dims)}." + ) + + for i in range(len(slice_size)): + size = slice_size[i] + dim = sliceable_head_dims[i] + if size is not None and size > dim: + raise ValueError(f"size {size} has to be smaller or equal to {dim}.") + + # Recursively walk through all the children. + # Any children which exposes the set_attention_slice method + # gets the message + def fn_recursive_set_attention_slice(module: torch.nn.Module, slice_size: List[int]): + if hasattr(module, "set_attention_slice"): + module.set_attention_slice(slice_size.pop()) + + for child in module.children(): + fn_recursive_set_attention_slice(child, slice_size) + + reversed_slice_size = list(reversed(slice_size)) + for module in self.children(): + fn_recursive_set_attention_slice(module, reversed_slice_size) + + def _set_gradient_checkpointing(self, module, value=False): + if isinstance(module, (CrossAttnDownBlock2D, DownBlock2D)): + module.gradient_checkpointing = value + + def forward( + self, + sample: torch.FloatTensor, + timestep: Union[torch.Tensor, float, int], + encoder_hidden_states: torch.Tensor, + controlnet_cond: torch.FloatTensor, + conditioning_scale: float = 1.0, + class_labels: Optional[torch.Tensor] = None, + timestep_cond: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + added_cond_kwargs: Optional[Dict[str, torch.Tensor]] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + encoder_attention_mask: Optional[torch.Tensor] = None, + guess_mode: bool = False, + return_dict: bool = True, + ) -> Union[ControlNetOutput, Tuple]: + """ + The [`ControlNetModel`] forward method. + + Args: + sample (`torch.FloatTensor`): + The noisy input tensor. + timestep (`Union[torch.Tensor, float, int]`): + The number of timesteps to denoise an input. + encoder_hidden_states (`torch.Tensor`): + The encoder hidden states. + controlnet_cond (`torch.FloatTensor`): + The conditional input tensor of shape `(batch_size, sequence_length, hidden_size)`. + conditioning_scale (`float`, defaults to `1.0`): + The scale factor for ControlNet outputs. + class_labels (`torch.Tensor`, *optional*, defaults to `None`): + Optional class labels for conditioning. Their embeddings will be summed with the timestep embeddings. + timestep_cond (`torch.Tensor`, *optional*, defaults to `None`): + attention_mask (`torch.Tensor`, *optional*, defaults to `None`): + added_cond_kwargs (`dict`): + Additional conditions for the Stable Diffusion XL UNet. + cross_attention_kwargs (`dict[str]`, *optional*, defaults to `None`): + A kwargs dictionary that if specified is passed along to the `AttnProcessor`. + encoder_attention_mask (`torch.Tensor`): + A cross-attention mask of shape `(batch, sequence_length)` is applied to `encoder_hidden_states`. If + `True` the mask is kept, otherwise if `False` it is discarded. Mask will be converted into a bias, + which adds large negative values to the attention scores corresponding to "discard" tokens. + guess_mode (`bool`, defaults to `False`): + In this mode, the ControlNet encoder tries its best to recognize the input content of the input even if + you remove all prompts. A `guidance_scale` between 3.0 and 5.0 is recommended. + return_dict (`bool`, defaults to `True`): + Whether or not to return a [`~models.controlnet.ControlNetOutput`] instead of a plain tuple. + + Returns: + [`~models.controlnet.ControlNetOutput`] **or** `tuple`: + If `return_dict` is `True`, a [`~models.controlnet.ControlNetOutput`] is returned, otherwise a tuple is + returned where the first element is the sample tensor. + """ + # check channel order + channel_order = self.config.controlnet_conditioning_channel_order + + if channel_order == "rgb": + # in rgb order by default + ... + elif channel_order == "bgr": + controlnet_cond = torch.flip(controlnet_cond, dims=[1]) + else: + raise ValueError(f"unknown `controlnet_conditioning_channel_order`: {channel_order}") + + # prepare attention_mask + if attention_mask is not None: + attention_mask = (1 - attention_mask.to(sample.dtype)) * -10000.0 + attention_mask = attention_mask.unsqueeze(1) + + # convert encoder_attention_mask to a bias the same way we do for attention_mask + if encoder_attention_mask is not None: + encoder_attention_mask = (1 - encoder_attention_mask.to(sample.dtype)) * -10000.0 + encoder_attention_mask = encoder_attention_mask.unsqueeze(1) + + # 1. time + timesteps = timestep + if not torch.is_tensor(timesteps): + # TODO: this requires sync between CPU and GPU. So try to pass timesteps as tensors if you can + # This would be a good case for the `match` statement (Python 3.10+) + is_mps = sample.device.type == "mps" + if isinstance(timestep, float): + dtype = torch.float32 if is_mps else torch.float64 + else: + dtype = torch.int32 if is_mps else torch.int64 + timesteps = torch.tensor([timesteps], dtype=dtype, device=sample.device) + elif len(timesteps.shape) == 0: + timesteps = timesteps[None].to(sample.device) + + # broadcast to batch dimension in a way that's compatible with ONNX/Core ML + timesteps = timesteps.expand(sample.shape[0]) + + t_emb = self.time_proj(timesteps) + + # timesteps does not contain any weights and will always return f32 tensors + # but time_embedding might actually be running in fp16. so we need to cast here. + # there might be better ways to encapsulate this. + t_emb = t_emb.to(dtype=sample.dtype) + + emb = self.time_embedding(t_emb, timestep_cond) + aug_emb = None + + if self.class_embedding is not None: + if class_labels is None: + raise ValueError("class_labels should be provided when num_class_embeds > 0") + + if self.config.class_embed_type == "timestep": + class_labels = self.time_proj(class_labels) + + class_emb = self.class_embedding(class_labels).to(dtype=self.dtype) + emb = emb + class_emb + + if "addition_embed_type" in self.config: + if self.config.addition_embed_type == "text": + aug_emb = self.add_embedding(encoder_hidden_states) + + elif self.config.addition_embed_type == "text_time": + if "text_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'text_time' which \ + requires the keyword argument `text_embeds` to be passed in `added_cond_kwargs`" + ) + text_embeds = added_cond_kwargs.get("text_embeds") + if "time_ids" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'text_time' which \ + requires the keyword argument `time_ids` to be passed in `added_cond_kwargs`" + ) + time_ids = added_cond_kwargs.get("time_ids") + time_embeds = self.add_time_proj(time_ids.flatten()) + time_embeds = time_embeds.reshape((text_embeds.shape[0], -1)) + + add_embeds = torch.concat([text_embeds, time_embeds], dim=-1) + add_embeds = add_embeds.to(emb.dtype) + aug_emb = self.add_embedding(add_embeds) + + emb = emb + aug_emb if aug_emb is not None else emb + + # 2. pre-process + sample = self.conv_in(sample) + + controlnet_cond = self.controlnet_cond_embedding(controlnet_cond) + sample = sample + controlnet_cond + + # 3. down + down_block_res_samples = (sample,) + for downsample_block in self.down_blocks: + if hasattr(downsample_block, "has_cross_attention") and downsample_block.has_cross_attention: + sample, res_samples = downsample_block( + hidden_states=sample, + temb=emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + ) + else: + sample, res_samples = downsample_block(hidden_states=sample, temb=emb) + + down_block_res_samples += res_samples + + # 4. mid + if self.mid_block is not None: + sample = self.mid_block( + sample, + emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + ) + + # 5. Control net blocks + + controlnet_down_block_res_samples = () + + for down_block_res_sample, controlnet_block in zip( + down_block_res_samples, self.controlnet_down_blocks, strict=True + ): + down_block_res_sample = controlnet_block(down_block_res_sample) + controlnet_down_block_res_samples = controlnet_down_block_res_samples + (down_block_res_sample,) + + down_block_res_samples = controlnet_down_block_res_samples + + mid_block_res_sample = self.controlnet_mid_block(sample) + + # 6. scaling + if guess_mode and not self.config.global_pool_conditions: + scales = torch.logspace(-1, 0, len(down_block_res_samples) + 1, device=sample.device) # 0.1 to 1.0 + + scales = scales * conditioning_scale + down_block_res_samples = [ + sample * scale for sample, scale in zip(down_block_res_samples, scales, strict=False) + ] + mid_block_res_sample = mid_block_res_sample * scales[-1] # last one + else: + down_block_res_samples = [sample * conditioning_scale for sample in down_block_res_samples] + mid_block_res_sample = mid_block_res_sample * conditioning_scale + + if self.config.global_pool_conditions: + down_block_res_samples = [torch.mean(sample, dim=(2, 3), keepdim=True) for sample in down_block_res_samples] + mid_block_res_sample = torch.mean(mid_block_res_sample, dim=(2, 3), keepdim=True) + + if not return_dict: + return (down_block_res_samples, mid_block_res_sample) + + return ControlNetOutput( + down_block_res_samples=down_block_res_samples, mid_block_res_sample=mid_block_res_sample + ) + + +diffusers.ControlNetModel = ControlNetModel +diffusers.models.controlnets.controlnet.ControlNetModel = ControlNetModel + + +# patch LoRACompatibleConv to use original Conv2D forward function +# this needed to make work seamless patch +# NOTE: with this patch, torch.compile crashes on 2.0 torch(already fixed in nightly) +# https://github.com/huggingface/diffusers/pull/4315 +# https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/lora.py#L96C18-L96C18 +def new_LoRACompatibleConv_forward(self, hidden_states, scale: float = 1.0): + if self.lora_layer is None: + return super(diffusers.models.lora.LoRACompatibleConv, self).forward(hidden_states) + else: + return super(diffusers.models.lora.LoRACompatibleConv, self).forward(hidden_states) + ( + scale * self.lora_layer(hidden_states) + ) + + +diffusers.models.lora.LoRACompatibleConv.forward = new_LoRACompatibleConv_forward + +try: + import xformers + + xformers_available = True +except Exception: + xformers_available = False + + +if xformers_available: + # TODO: remove when fixed in diffusers + _xformers_memory_efficient_attention = xformers.ops.memory_efficient_attention + + def new_memory_efficient_attention( + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + attn_bias=None, + p: float = 0.0, + scale: Optional[float] = None, + *, + op=None, + ): + # diffusers not align shape to 8, which is required by xformers + if attn_bias is not None and type(attn_bias) is torch.Tensor: + orig_size = attn_bias.shape[-1] + new_size = ((orig_size + 7) // 8) * 8 + aligned_attn_bias = torch.zeros( + (attn_bias.shape[0], attn_bias.shape[1], new_size), + device=attn_bias.device, + dtype=attn_bias.dtype, + ) + aligned_attn_bias[:, :, :orig_size] = attn_bias + attn_bias = aligned_attn_bias[:, :, :orig_size] + + return _xformers_memory_efficient_attention( + query=query, + key=key, + value=value, + attn_bias=attn_bias, + p=p, + scale=scale, + op=op, + ) + + xformers.ops.memory_efficient_attention = new_memory_efficient_attention diff --git a/invokeai/backend/util/logging.py b/invokeai/backend/util/logging.py new file mode 100644 index 00000000000..968604eb3d9 --- /dev/null +++ b/invokeai/backend/util/logging.py @@ -0,0 +1,435 @@ +# Copyright (c) 2023 Lincoln D. Stein and The InvokeAI Development Team + +""" +Logging class for InvokeAI that produces console messages. + +Usage: + +from invokeai.backend.util.logging import InvokeAILogger + +logger = InvokeAILogger.get_logger(name='InvokeAI') // Initialization +(or) +logger = InvokeAILogger.get_logger(__name__) // To use the filename +logger.configure() + +logger.critical('this is critical') // Critical Message +logger.error('this is an error') // Error Message +logger.warning('this is a warning') // Warning Message +logger.info('this is info') // Info Message +logger.debug('this is debugging') // Debug Message + +Console messages: + [12-05-2023 20]::[InvokeAI]::CRITICAL --> This is an info message [In Bold Red] + [12-05-2023 20]::[InvokeAI]::ERROR --> This is an info message [In Red] + [12-05-2023 20]::[InvokeAI]::WARNING --> This is an info message [In Yellow] + [12-05-2023 20]::[InvokeAI]::INFO --> This is an info message [In Grey] + [12-05-2023 20]::[InvokeAI]::DEBUG --> This is an info message [In Grey] + +Alternate Method (in this case the logger name will be set to InvokeAI): +import invokeai.backend.util.logging as IAILogger +IAILogger.debug('this is a debugging message') + +## Configuration + +The default configuration will print to stderr on the console. To add +additional logging handlers, call get_logger with an initialized InvokeAIAppConfig +object: + + + config = InvokeAIAppConfig.get_config() + config.parse_args() + logger = InvokeAILogger.get_logger(config=config) + +### Three command-line options control logging: + +`--log_handlers ...` + +This option activates one or more log handlers. Options are "console", "file", "syslog" and "http". To specify more than one, separate them by spaces: + +``` +invokeai-web --log_handlers console syslog=/dev/log file=C:\\Users\\fred\\invokeai.log +``` + +The format of these options is described below. + +### `--log_format {plain|color|legacy|syslog}` + +This controls the format of log messages written to the console. Only the "console" log handler is currently affected by this setting. + +* "plain" provides formatted messages like this: + +```bash + +[2023-05-24 23:18:2[2023-05-24 23:18:50,352]::[InvokeAI]::DEBUG --> this is a debug message +[2023-05-24 23:18:50,352]::[InvokeAI]::INFO --> this is an informational messages +[2023-05-24 23:18:50,352]::[InvokeAI]::WARNING --> this is a warning +[2023-05-24 23:18:50,352]::[InvokeAI]::ERROR --> this is an error +[2023-05-24 23:18:50,352]::[InvokeAI]::CRITICAL --> this is a critical error +``` + +* "color" produces similar output, but the text will be color coded to indicate the severity of the message. + +* "legacy" produces output similar to InvokeAI versions 2.3 and earlier: + +``` +### this is a critical error +*** this is an error +** this is a warning +>> this is an informational messages + | this is a debug message +``` + +* "syslog" produces messages suitable for syslog entries: + +```bash +InvokeAI [2691178] this is a critical error +InvokeAI [2691178] this is an error +InvokeAI [2691178] this is a warning +InvokeAI [2691178] this is an informational messages +InvokeAI [2691178] this is a debug message +``` + +(note that the date, time and hostname will be added by the syslog system) + +### `--log_level {debug|info|warning|error|critical}` + +Providing this command-line option will cause only messages at the specified level or above to be emitted. + +## Console logging + +When "console" is provided to `--log_handlers`, messages will be written to the command line window in which InvokeAI was launched. By default, the color formatter will be used unless overridden by `--log_format`. + +## File logging + +When "file" is provided to `--log_handlers`, entries will be written to the file indicated in the path argument. By default, the "plain" format will be used: + +```bash +invokeai-web --log_handlers file=/var/log/invokeai.log +``` + +## Syslog logging + +When "syslog" is requested, entries will be sent to the syslog system. There are a variety of ways to control where the log message is sent: + +* Send to the local machine using the `/dev/log` socket: + +``` +invokeai-web --log_handlers syslog=/dev/log +``` + +* Send to the local machine using a UDP message: + +``` +invokeai-web --log_handlers syslog=localhost +``` + +* Send to the local machine using a UDP message on a nonstandard port: + +``` +invokeai-web --log_handlers syslog=localhost:512 +``` + +* Send to a remote machine named "loghost" on the local LAN using facility LOG_USER and UDP packets: + +``` +invokeai-web --log_handlers syslog=loghost,facility=LOG_USER,socktype=SOCK_DGRAM +``` + +This can be abbreviated `syslog=loghost`, as LOG_USER and SOCK_DGRAM are defaults. + +* Send to a remote machine named "loghost" using the facility LOCAL0 and using a TCP socket: + +``` +invokeai-web --log_handlers syslog=loghost,facility=LOG_LOCAL0,socktype=SOCK_STREAM +``` + +If no arguments are specified (just a bare "syslog"), then the logging system will look for a UNIX socket named `/dev/log`, and if not found try to send a UDP message to `localhost`. The Macintosh OS used to support logging to a socket named `/var/run/syslog`, but this feature has since been disabled. + +## Web logging + +If you have access to a web server that is configured to log messages when a particular URL is requested, you can log using the "http" method: + +``` +invokeai-web --log_handlers http=http://my.server/path/to/logger,method=POST +``` + +The optional [,method=] part can be used to specify whether the URL accepts GET (default) or POST messages. + +Currently password authentication and SSL are not supported. + +## Using the configuration file + +You can set and forget logging options by adding a "Logging" section to `invokeai.yaml`: + +``` +InvokeAI: + [... other settings...] + Logging: + log_handlers: + - console + - syslog=/dev/log + log_level: info + log_format: color +``` + +""" + +import logging.handlers +import socket +import urllib.parse +from pathlib import Path +from typing import Any, Dict, Optional + +from invokeai.app.services.config import InvokeAIAppConfig +from invokeai.app.services.config.config_default import get_config + +try: + import syslog + + SYSLOG_AVAILABLE = True +except ImportError: + SYSLOG_AVAILABLE = False + + +# module level functions +def debug(msg: str, *args: str, **kwargs: Any) -> None: # noqa D103 + InvokeAILogger.get_logger().debug(msg, *args, **kwargs) + + +def info(msg: str, *args: str, **kwargs: Any) -> None: # noqa D103 + InvokeAILogger.get_logger().info(msg, *args, **kwargs) + + +def warning(msg: str, *args: str, **kwargs: Any) -> None: # noqa D103 + InvokeAILogger.get_logger().warning(msg, *args, **kwargs) + + +def error(msg: str, *args: str, **kwargs: Any) -> None: # noqa D103 + InvokeAILogger.get_logger().error(msg, *args, **kwargs) + + +def critical(msg: str, *args: str, **kwargs: Any) -> None: # noqa D103 + InvokeAILogger.get_logger().critical(msg, *args, **kwargs) + + +def log(level: int, msg: str, *args: str, **kwargs: Any) -> None: # noqa D103 + InvokeAILogger.get_logger().log(level, msg, *args, **kwargs) + + +def disable(level: int = logging.CRITICAL) -> None: # noqa D103 + logging.disable(level) + + +def basicConfig(**kwargs: Any) -> None: # noqa D103 + logging.basicConfig(**kwargs) + + +_FACILITY_MAP = ( + { + "LOG_KERN": syslog.LOG_KERN, + "LOG_USER": syslog.LOG_USER, + "LOG_MAIL": syslog.LOG_MAIL, + "LOG_DAEMON": syslog.LOG_DAEMON, + "LOG_AUTH": syslog.LOG_AUTH, + "LOG_LPR": syslog.LOG_LPR, + "LOG_NEWS": syslog.LOG_NEWS, + "LOG_UUCP": syslog.LOG_UUCP, + "LOG_CRON": syslog.LOG_CRON, + "LOG_SYSLOG": syslog.LOG_SYSLOG, + "LOG_LOCAL0": syslog.LOG_LOCAL0, + "LOG_LOCAL1": syslog.LOG_LOCAL1, + "LOG_LOCAL2": syslog.LOG_LOCAL2, + "LOG_LOCAL3": syslog.LOG_LOCAL3, + "LOG_LOCAL4": syslog.LOG_LOCAL4, + "LOG_LOCAL5": syslog.LOG_LOCAL5, + "LOG_LOCAL6": syslog.LOG_LOCAL6, + "LOG_LOCAL7": syslog.LOG_LOCAL7, + } + if SYSLOG_AVAILABLE + else {} +) + +_SOCK_MAP = { + "SOCK_STREAM": socket.SOCK_STREAM, + "SOCK_DGRAM": socket.SOCK_DGRAM, +} + + +class InvokeAIFormatter(logging.Formatter): + """Base class for logging formatter.""" + + def format(self, record: logging.LogRecord) -> str: # noqa D102 + formatter = logging.Formatter(self.log_fmt(record.levelno)) + return formatter.format(record) + + def log_fmt(self, levelno: int) -> str: # noqa D102 + return "[%(asctime)s]::[%(name)s]::%(levelname)s --> %(message)s" + + +class InvokeAISyslogFormatter(InvokeAIFormatter): + """Formatting for syslog.""" + + def log_fmt(self, levelno: int) -> str: # noqa D102 + return "%(name)s [%(process)d] <%(levelname)s> %(message)s" + + +class InvokeAILegacyLogFormatter(InvokeAIFormatter): # noqa D102 + """Formatting for the InvokeAI Logger (legacy version).""" + + FORMATS = { + logging.DEBUG: " | %(message)s", + logging.INFO: ">> %(message)s", + logging.WARNING: "** %(message)s", + logging.ERROR: "*** %(message)s", + logging.CRITICAL: "### %(message)s", + } + + def log_fmt(self, levelno: int) -> str: # noqa D102 + format = self.FORMATS.get(levelno) + assert format is not None + return format + + +class InvokeAIPlainLogFormatter(InvokeAIFormatter): + """Custom Formatting for the InvokeAI Logger (plain version).""" + + def log_fmt(self, levelno: int) -> str: # noqa D102 + return "[%(asctime)s]::[%(name)s]::%(levelname)s --> %(message)s" + + +class InvokeAIColorLogFormatter(InvokeAIFormatter): + """Custom Formatting for the InvokeAI Logger.""" + + # Color Codes + grey = "\x1b[38;20m" + yellow = "\x1b[33;20m" + red = "\x1b[31;20m" + cyan = "\x1b[36;20m" + bold_red = "\x1b[31;1m" + reset = "\x1b[0m" + + # Log Format + log_format = "[%(asctime)s]::[%(name)s]::%(levelname)s --> %(message)s" + ## More Formatting Options: %(pathname)s, %(filename)s, %(module)s, %(lineno)d + + # Format Map + FORMATS = { + logging.DEBUG: cyan + log_format + reset, + logging.INFO: grey + log_format + reset, + logging.WARNING: yellow + log_format + reset, + logging.ERROR: red + log_format + reset, + logging.CRITICAL: bold_red + log_format + reset, + } + + def log_fmt(self, levelno: int) -> str: # noqa D102 + format = self.FORMATS.get(levelno) + assert format is not None + return format + + +LOG_FORMATTERS = { + "plain": InvokeAIPlainLogFormatter, + "color": InvokeAIColorLogFormatter, + "syslog": InvokeAISyslogFormatter, + "legacy": InvokeAILegacyLogFormatter, +} + + +class InvokeAILogger(object): # noqa D102 + loggers: Dict[str, logging.Logger] = {} + + @classmethod + def get_logger(cls, name: str = "InvokeAI", config: Optional[InvokeAIAppConfig] = None) -> logging.Logger: # noqa D102 + config = config or get_config() + if name in cls.loggers: + return cls.loggers[name] + + logger = logging.getLogger(name) + logger.setLevel(config.log_level.upper()) # yes, strings work here + for ch in cls.get_loggers(config): + logger.addHandler(ch) + cls.loggers[name] = logger + return cls.loggers[name] + + @classmethod + def get_loggers(cls, config: InvokeAIAppConfig) -> list[logging.Handler]: # noqa D102 + handler_strs = config.log_handlers + handlers = [] + for handler in handler_strs: + handler_name, *args = handler.split("=", 2) + arg = args[0] if len(args) > 0 else None + + # console and file get the fancy formatter. + # syslog gets a simple one + # http gets no custom formatter + formatter = LOG_FORMATTERS[config.log_format] + if handler_name == "console": + ch: logging.Handler = logging.StreamHandler() + ch.setFormatter(formatter()) + handlers.append(ch) + + elif handler_name == "syslog": + ch = cls._parse_syslog_args(arg) + handlers.append(ch) + + elif handler_name == "file": + ch = cls._parse_file_args(arg) + ch.setFormatter(formatter()) + handlers.append(ch) + + elif handler_name == "http": + ch = cls._parse_http_args(arg) + handlers.append(ch) + return handlers + + @staticmethod + def _parse_syslog_args(args: Optional[str] = None) -> logging.Handler: + if not SYSLOG_AVAILABLE: + raise ValueError("syslog is not available on this system") + if not args: + args = "/dev/log" if Path("/dev/log").exists() else "address:localhost:514" + syslog_args: Dict[str, Any] = {} + try: + for a in args.split(","): + arg_name, *arg_value = a.split(":", 2) + if arg_name == "address": + host, *port_list = arg_value + port = 514 if not port_list else int(port_list[0]) + syslog_args["address"] = (host, port) + elif arg_name == "facility": + syslog_args["facility"] = _FACILITY_MAP[arg_value[0]] + elif arg_name == "socktype": + syslog_args["socktype"] = _SOCK_MAP[arg_value[0]] + else: + syslog_args["address"] = arg_name + except Exception: + raise ValueError(f"{args} is not a value argument list for syslog logging") + return logging.handlers.SysLogHandler(**syslog_args) + + @staticmethod + def _parse_file_args(args: Optional[str] = None) -> logging.Handler: # noqa D102 + if not args: + raise ValueError("please provide filename for file logging using format 'file=/path/to/logfile.txt'") + return logging.FileHandler(args) + + @staticmethod + def _parse_http_args(args: Optional[str] = None) -> logging.Handler: # noqa D102 + if not args: + raise ValueError("please provide destination for http logging using format 'http=url'") + arg_list = args.split(",") + url = urllib.parse.urlparse(arg_list.pop(0)) + if url.scheme != "http": + raise ValueError(f"the http logging module can only log to HTTP URLs, but {url.scheme} was specified") + host = url.hostname + path = url.path + port = url.port or 80 + + syslog_args: Dict[str, Any] = {} + for a in arg_list: + arg_name, *arg_value = a.split(":", 2) + if arg_name == "method": + method = arg_value[0] if len(arg_value) > 0 else "GET" + syslog_args[arg_name] = method + else: # TODO: Provide support for SSL context and credentials + pass + return logging.handlers.HTTPHandler(f"{host}:{port}", path, **syslog_args) diff --git a/invokeai/backend/util/mask.py b/invokeai/backend/util/mask.py new file mode 100644 index 00000000000..45aa32061c2 --- /dev/null +++ b/invokeai/backend/util/mask.py @@ -0,0 +1,53 @@ +import torch + + +def to_standard_mask_dim(mask: torch.Tensor) -> torch.Tensor: + """Standardize the dimensions of a mask tensor. + + Args: + mask (torch.Tensor): A mask tensor. The shape can be (1, h, w) or (h, w). + + Returns: + torch.Tensor: The output mask tensor. The shape is (1, h, w). + """ + # Get the mask height and width. + if mask.ndim == 2: + mask = mask.unsqueeze(0) + elif mask.ndim == 3 and mask.shape[0] == 1: + pass + else: + raise ValueError(f"Unsupported mask shape: {mask.shape}. Expected (1, h, w) or (h, w).") + + return mask + + +def to_standard_float_mask(mask: torch.Tensor, out_dtype: torch.dtype) -> torch.Tensor: + """Standardize the format of a mask tensor. + + Args: + mask (torch.Tensor): A mask tensor. The dtype can be any bool, float, or int type. The shape must be (1, h, w) + or (h, w). + + out_dtype (torch.dtype): The dtype of the output mask tensor. Must be a float type. + + Returns: + torch.Tensor: The output mask tensor. The dtype is out_dtype. The shape is (1, h, w). All values are either 0.0 + or 1.0. + """ + + if not out_dtype.is_floating_point: + raise ValueError(f"out_dtype must be a float type, but got {out_dtype}") + + mask = to_standard_mask_dim(mask) + mask = mask.to(out_dtype) + + # Set masked regions to 1.0. + if mask.dtype == torch.bool: + mask = mask.to(out_dtype) + else: + mask = mask.to(out_dtype) + mask_region = mask > 0.5 + mask[mask_region] = 1.0 + mask[~mask_region] = 0.0 + + return mask diff --git a/invokeai/backend/util/original_weights_storage.py b/invokeai/backend/util/original_weights_storage.py new file mode 100644 index 00000000000..af945b086f5 --- /dev/null +++ b/invokeai/backend/util/original_weights_storage.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from typing import Dict, Iterator, Optional, Tuple + +import torch + +from invokeai.backend.util.devices import TorchDevice + + +class OriginalWeightsStorage: + """A class for tracking the original weights of a model for patch/unpatch operations.""" + + def __init__(self, cached_weights: Optional[Dict[str, torch.Tensor]] = None): + # The original weights of the model. + self._weights: dict[str, torch.Tensor] = {} + # The keys of the weights that have been changed (via `save()`) during the lifetime of this instance. + self._changed_weights: set[str] = set() + if cached_weights: + self._weights.update(cached_weights) + + def save(self, key: str, weight: torch.Tensor, copy: bool = True): + self._changed_weights.add(key) + if key in self._weights: + return + + self._weights[key] = weight.detach().to(device=TorchDevice.CPU_DEVICE, copy=copy) + + def get(self, key: str, copy: bool = False) -> Optional[torch.Tensor]: + weight = self._weights.get(key, None) + if weight is not None and copy: + weight = weight.clone() + return weight + + def contains(self, key: str) -> bool: + return key in self._weights + + def get_changed_weights(self) -> Iterator[Tuple[str, torch.Tensor]]: + for key in self._changed_weights: + yield key, self._weights[key] diff --git a/invokeai/backend/util/prefix_logger_adapter.py b/invokeai/backend/util/prefix_logger_adapter.py new file mode 100644 index 00000000000..94f0478c95d --- /dev/null +++ b/invokeai/backend/util/prefix_logger_adapter.py @@ -0,0 +1,12 @@ +import logging +from typing import Any, MutableMapping + + +# Issue with type hints related to LoggerAdapter: https://github.com/python/typeshed/issues/7855 +class PrefixedLoggerAdapter(logging.LoggerAdapter): # type: ignore + def __init__(self, logger: logging.Logger, prefix: str): + super().__init__(logger, {}) + self.prefix = prefix + + def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, MutableMapping[str, Any]]: + return f"[{self.prefix}] {msg}", kwargs diff --git a/invokeai/backend/util/silence_warnings.py b/invokeai/backend/util/silence_warnings.py new file mode 100644 index 00000000000..0cd6d0738d0 --- /dev/null +++ b/invokeai/backend/util/silence_warnings.py @@ -0,0 +1,36 @@ +import warnings +from contextlib import ContextDecorator + +from diffusers.utils import logging as diffusers_logging +from transformers import logging as transformers_logging + + +# Inherit from ContextDecorator to allow using SilenceWarnings as both a context manager and a decorator. +class SilenceWarnings(ContextDecorator): + """A context manager that disables warnings from transformers & diffusers modules while active. + + As context manager: + ``` + with SilenceWarnings(): + # do something + ``` + + As decorator: + ``` + @SilenceWarnings() + def some_function(): + # do something + ``` + """ + + def __enter__(self) -> None: + self._transformers_verbosity = transformers_logging.get_verbosity() + self._diffusers_verbosity = diffusers_logging.get_verbosity() + transformers_logging.set_verbosity_error() + diffusers_logging.set_verbosity_error() + warnings.simplefilter("ignore") + + def __exit__(self, *args) -> None: + transformers_logging.set_verbosity(self._transformers_verbosity) + diffusers_logging.set_verbosity(self._diffusers_verbosity) + warnings.simplefilter("default") diff --git a/invokeai/backend/util/test_utils.py b/invokeai/backend/util/test_utils.py new file mode 100644 index 00000000000..e4208dc848f --- /dev/null +++ b/invokeai/backend/util/test_utils.py @@ -0,0 +1,64 @@ +import contextlib +from pathlib import Path +from typing import Optional, Union + +import pytest +import torch + +from invokeai.app.services.model_manager import ModelManagerServiceBase +from invokeai.app.services.model_records import UnknownModelException +from invokeai.backend.model_manager.load.load_base import LoadedModel +from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType + + +@pytest.fixture(scope="session") +def torch_device(): + return "cuda" if torch.cuda.is_available() else "cpu" + + +def install_and_load_model( + model_manager: ModelManagerServiceBase, + model_path_id_or_url: Union[str, Path], + model_name: str, + base_model: BaseModelType, + model_type: ModelType, + submodel_type: Optional[SubModelType] = None, +) -> LoadedModel: + """Install a model if it is not already installed, then get the LoadedModel for that model. + + This is intended as a utility function for tests. + + Args: + mm2_model_manager (ModelManagerServiceBase): The model manager + model_path_id_or_url (Union[str, Path]): The path, HF ID, URL, etc. where the model can be installed from if it + is not already installed. + model_name (str): The model name, forwarded to ModelManager.get_model(...). + base_model (BaseModelType): The base model, forwarded to ModelManager.get_model(...). + model_type (ModelType): The model type, forwarded to ModelManager.get_model(...). + submodel_type (Optional[SubModelType]): The submodel type, forwarded to ModelManager.get_model(...). + + Returns: + LoadedModelInfo + """ + # If the requested model is already installed, return its LoadedModel + with contextlib.suppress(UnknownModelException): + # TODO: Replace with wrapper call + configs = model_manager.store.search_by_attr( + model_name=model_name, base_model=base_model, model_type=model_type + ) + loaded_model: LoadedModel = model_manager.load.load_model(configs[0]) + return loaded_model + + # Install the requested model. + job = model_manager.install.heuristic_import(model_path_id_or_url) + model_manager.install.wait_for_job(job, timeout=10) + assert job.complete + + try: + loaded_model = model_manager.load.load_model(job.config_out) + return loaded_model + except UnknownModelException as e: + raise Exception( + "Failed to get model info after installing it. There could be a mismatch between the requested model and" + f" the installation id ('{model_path_id_or_url}'). Error: {e}" + ) diff --git a/invokeai/backend/util/util.py b/invokeai/backend/util/util.py new file mode 100644 index 00000000000..fb8671cec29 --- /dev/null +++ b/invokeai/backend/util/util.py @@ -0,0 +1,74 @@ +import base64 +import io +import os +import re +import unicodedata +from pathlib import Path + +from PIL import Image + + +def slugify(value: str, allow_unicode: bool = False) -> str: + """ + Convert to ASCII if 'allow_unicode' is False. Convert spaces or repeated + dashes to single dashes. Remove characters that aren't alphanumerics, + underscores, or hyphens. Replace slashes with underscores. + Convert to lowercase. Also strip leading and + trailing whitespace, dashes, and underscores. + + Adapted from Django: https://github.com/django/django/blob/main/django/utils/text.py + """ + value = str(value) + if allow_unicode: + value = unicodedata.normalize("NFKC", value) + else: + value = unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii") + value = re.sub(r"[/]", "_", value.lower()) + value = re.sub(r"[^.\w\s-]", "", value.lower()) + return re.sub(r"[-\s]+", "-", value).strip("-_") + + +def safe_filename(directory: Path, value: str) -> str: + """Make a string safe to use as a filename.""" + escaped_string = slugify(value) + max_name_length = os.pathconf(directory, "PC_NAME_MAX") if hasattr(os, "pathconf") else 256 + return escaped_string[len(escaped_string) - max_name_length :] + + +def directory_size(directory: Path) -> int: + """ + Return the aggregate size of all files in a directory (bytes). + """ + sum = 0 + for root, _, files in os.walk(directory): + for f in files: + sum += Path(root, f).stat().st_size + return sum + + +def image_to_dataURL(image: Image.Image, image_format: str = "PNG") -> str: + """ + Converts an image into a base64 image dataURL. + """ + buffered = io.BytesIO() + image.save(buffered, format=image_format) + mime_type = Image.MIME.get(image_format.upper(), "image/" + image_format.lower()) + image_base64 = f"data:{mime_type};base64," + base64.b64encode(buffered.getvalue()).decode("UTF-8") + return image_base64 + + +class Chdir(object): + """Context manager to chdir to desired directory and change back after context exits: + Args: + path (Path): The path to the cwd + """ + + def __init__(self, path: Path): + self.path = path + self.original = Path().absolute() + + def __enter__(self): + os.chdir(self.path) + + def __exit__(self, *args): + os.chdir(self.original) diff --git a/invokeai/backend/util/vae_working_memory.py b/invokeai/backend/util/vae_working_memory.py new file mode 100644 index 00000000000..f9228ced652 --- /dev/null +++ b/invokeai/backend/util/vae_working_memory.py @@ -0,0 +1,115 @@ +from typing import Literal + +import torch +from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL +from diffusers.models.autoencoders.autoencoder_tiny import AutoencoderTiny + +from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR +from invokeai.backend.flux.modules.autoencoder import AutoEncoder + + +def estimate_vae_working_memory_sd15_sdxl( + operation: Literal["encode", "decode"], + image_tensor: torch.Tensor, + vae: AutoencoderKL | AutoencoderTiny, + tile_size: int | None, + fp32: bool, +) -> int: + """Estimate the working memory required to encode or decode the given tensor.""" + # It was found experimentally that the peak working memory scales linearly with the number of pixels and the + # element size (precision). This estimate is accurate for both SD1 and SDXL. + element_size = 4 if fp32 else 2 + + # This constant is determined experimentally and takes into consideration both allocated and reserved memory. See #8414 + # Encoding uses ~45% the working memory as decoding. + scaling_constant = 2200 if operation == "decode" else 1100 + + latent_scale_factor_for_operation = LATENT_SCALE_FACTOR if operation == "decode" else 1 + + if tile_size is not None: + if tile_size == 0: + tile_size = vae.tile_sample_min_size + assert isinstance(tile_size, int) + h = tile_size + w = tile_size + working_memory = h * w * element_size * scaling_constant + + # We add 25% to the working memory estimate when tiling is enabled to account for factors like tile overlap + # and number of tiles. We could make this more precise in the future, but this should be good enough for + # most use cases. + working_memory = working_memory * 1.25 + else: + h = latent_scale_factor_for_operation * image_tensor.shape[-2] + w = latent_scale_factor_for_operation * image_tensor.shape[-1] + working_memory = h * w * element_size * scaling_constant + + if fp32: + # If we are running in FP32, then we should account for the likely increase in model size (~250MB). + working_memory += 250 * 2**20 + + return int(working_memory) + + +def estimate_vae_working_memory_cogview4( + operation: Literal["encode", "decode"], image_tensor: torch.Tensor, vae: AutoencoderKL +) -> int: + """Estimate the working memory required by the invocation in bytes.""" + latent_scale_factor_for_operation = LATENT_SCALE_FACTOR if operation == "decode" else 1 + + h = latent_scale_factor_for_operation * image_tensor.shape[-2] + w = latent_scale_factor_for_operation * image_tensor.shape[-1] + element_size = next(vae.parameters()).element_size() + + # This constant is determined experimentally and takes into consideration both allocated and reserved memory. See #8414 + # Encoding uses ~45% the working memory as decoding. + scaling_constant = 2200 if operation == "decode" else 1100 + working_memory = h * w * element_size * scaling_constant + + print(f"estimate_vae_working_memory_cogview4: {int(working_memory)}") + + return int(working_memory) + + +def estimate_vae_working_memory_flux( + operation: Literal["encode", "decode"], image_tensor: torch.Tensor, vae: AutoEncoder +) -> int: + """Estimate the working memory required by the invocation in bytes.""" + + latent_scale_factor_for_operation = LATENT_SCALE_FACTOR if operation == "decode" else 1 + + out_h = latent_scale_factor_for_operation * image_tensor.shape[-2] + out_w = latent_scale_factor_for_operation * image_tensor.shape[-1] + element_size = next(vae.parameters()).element_size() + + # This constant is determined experimentally and takes into consideration both allocated and reserved memory. See #8414 + # Encoding uses ~45% the working memory as decoding. + scaling_constant = 2200 if operation == "decode" else 1100 + + working_memory = out_h * out_w * element_size * scaling_constant + + print(f"estimate_vae_working_memory_flux: {int(working_memory)}") + + return int(working_memory) + + +def estimate_vae_working_memory_sd3( + operation: Literal["encode", "decode"], image_tensor: torch.Tensor, vae: AutoencoderKL +) -> int: + """Estimate the working memory required by the invocation in bytes.""" + # Encode operations use approximately 50% of the memory required for decode operations + + latent_scale_factor_for_operation = LATENT_SCALE_FACTOR if operation == "decode" else 1 + + h = latent_scale_factor_for_operation * image_tensor.shape[-2] + w = latent_scale_factor_for_operation * image_tensor.shape[-1] + element_size = next(vae.parameters()).element_size() + + # This constant is determined experimentally and takes into consideration both allocated and reserved memory. See #8414 + # Encoding uses ~45% the working memory as decoding. + scaling_constant = 2200 if operation == "decode" else 1100 + + working_memory = h * w * element_size * scaling_constant + + print(f"estimate_vae_working_memory_sd3: {int(working_memory)}") + + return int(working_memory) diff --git a/invokeai/backend/z_image/__init__.py b/invokeai/backend/z_image/__init__.py new file mode 100644 index 00000000000..7fe48dd2cb2 --- /dev/null +++ b/invokeai/backend/z_image/__init__.py @@ -0,0 +1,16 @@ +# Z-Image backend utilities +from invokeai.backend.z_image.z_image_control_adapter import ZImageControlAdapter +from invokeai.backend.z_image.z_image_control_transformer import ZImageControlTransformer2DModel +from invokeai.backend.z_image.z_image_controlnet_extension import ( + ZImageControlNetExtension, + z_image_forward_with_control, +) +from invokeai.backend.z_image.z_image_patchify_utils import patchify_control_context + +__all__ = [ + "ZImageControlAdapter", + "ZImageControlTransformer2DModel", + "ZImageControlNetExtension", + "z_image_forward_with_control", + "patchify_control_context", +] diff --git a/invokeai/backend/z_image/extensions/__init__.py b/invokeai/backend/z_image/extensions/__init__.py new file mode 100644 index 00000000000..318b401c79c --- /dev/null +++ b/invokeai/backend/z_image/extensions/__init__.py @@ -0,0 +1 @@ +# Z-Image extensions diff --git a/invokeai/backend/z_image/extensions/regional_prompting_extension.py b/invokeai/backend/z_image/extensions/regional_prompting_extension.py new file mode 100644 index 00000000000..26f91749f70 --- /dev/null +++ b/invokeai/backend/z_image/extensions/regional_prompting_extension.py @@ -0,0 +1,205 @@ +from typing import Optional + +import torch +import torchvision + +from invokeai.backend.util.devices import TorchDevice +from invokeai.backend.util.mask import to_standard_float_mask +from invokeai.backend.z_image.text_conditioning import ZImageRegionalTextConditioning, ZImageTextConditioning + + +class ZImageRegionalPromptingExtension: + """A class for managing regional prompting with Z-Image. + + This implementation is inspired by the FLUX regional prompting extension and + the paper https://arxiv.org/pdf/2411.02395. + + Key difference from FLUX: Z-Image uses sequence order [img_tokens, txt_tokens], + while FLUX uses [txt_tokens, img_tokens]. The attention mask construction + accounts for this difference. + """ + + def __init__( + self, + regional_text_conditioning: ZImageRegionalTextConditioning, + regional_attn_mask: torch.Tensor | None = None, + ): + self.regional_text_conditioning = regional_text_conditioning + self.regional_attn_mask = regional_attn_mask + + def get_attn_mask(self, block_index: int) -> torch.Tensor | None: + """Get the 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. + """ + order = [self.regional_attn_mask, None] + return order[block_index % len(order)] + + @classmethod + def from_text_conditionings( + cls, + text_conditionings: list[ZImageTextConditioning], + img_seq_len: int, + ) -> "ZImageRegionalPromptingExtension": + """Create a ZImageRegionalPromptingExtension from a list of text conditionings. + + Args: + text_conditionings: List of text conditionings with optional masks. + img_seq_len: The image sequence length (i.e. (H // patch_size) * (W // patch_size)). + + Returns: + A configured ZImageRegionalPromptingExtension. + """ + regional_text_conditioning = ZImageRegionalTextConditioning.from_text_conditionings(text_conditionings) + attn_mask = cls._prepare_regional_attn_mask(regional_text_conditioning, img_seq_len) + return cls( + regional_text_conditioning=regional_text_conditioning, + regional_attn_mask=attn_mask, + ) + + @classmethod + def _prepare_regional_attn_mask( + cls, + regional_text_conditioning: ZImageRegionalTextConditioning, + img_seq_len: int, + ) -> torch.Tensor | None: + """Prepare a regional attention mask for Z-Image. + + This uses an 'unrestricted' image self-attention approach (similar to FLUX): + - Image tokens can attend to ALL other image tokens (unrestricted self-attention) + - Image tokens attend only to their corresponding regional text + - Text tokens attend only to their corresponding regional image + - Text tokens attend to themselves + + The unrestricted image self-attention allows the model to maintain global + coherence across regions, preventing the generation of separate/disconnected + images for each region. + + Z-Image sequence order: [img_tokens, txt_tokens] + + Args: + regional_text_conditioning: The regional text conditioning data. + img_seq_len: Number of image tokens. + + Returns: + Attention mask of shape (img_seq_len + txt_seq_len, img_seq_len + txt_seq_len). + Returns None if no regional masks are present. + """ + # Check if any regional masks exist + has_regional_masks = any(mask is not None for mask in regional_text_conditioning.image_masks) + if not has_regional_masks: + # No regional masks, return None to use default attention + 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: + # image_mask shape: (1, 1, img_seq_len) -> flatten to (img_seq_len,) + 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() + txt_seq_len = regional_text_conditioning.prompt_embeds.shape[0] + total_seq_len = img_seq_len + txt_seq_len + + # Initialize empty attention mask + # Z-Image sequence: [img_tokens (0:img_seq_len), txt_tokens (img_seq_len:total_seq_len)] + regional_attention_mask = torch.zeros((total_seq_len, total_seq_len), device=device, dtype=torch.float16) + + for image_mask, embedding_range in zip( + regional_text_conditioning.image_masks, + regional_text_conditioning.embedding_ranges, + strict=True, + ): + # Calculate text token positions in the unified sequence + txt_start = img_seq_len + embedding_range.start + txt_end = img_seq_len + embedding_range.end + + # 1. txt attends to itself + regional_attention_mask[txt_start:txt_end, txt_start:txt_end] = 1.0 + + if image_mask is not None: + # Flatten mask: (1, 1, img_seq_len) -> (img_seq_len,) + mask_flat = image_mask.view(img_seq_len) + + # 2. img attends to corresponding regional txt + # Reshape mask to (img_seq_len, 1) for broadcasting + regional_attention_mask[:img_seq_len, txt_start:txt_end] = mask_flat.view(img_seq_len, 1) + + # 3. txt attends to corresponding regional img + # Reshape mask to (1, img_seq_len) for broadcasting + regional_attention_mask[txt_start:txt_end, :img_seq_len] = mask_flat.view(1, img_seq_len) + else: + # Global prompt: allow attention to/from background regions only + if background_region_mask is not None: + # 2. background img attends to global txt + regional_attention_mask[:img_seq_len, txt_start:txt_end] = background_region_mask.view( + img_seq_len, 1 + ) + + # 3. global txt attends to background img + regional_attention_mask[txt_start:txt_end, :img_seq_len] = background_region_mask.view( + 1, img_seq_len + ) + else: + # No regional masks at all, allow full attention + regional_attention_mask[:img_seq_len, txt_start:txt_end] = 1.0 + regional_attention_mask[txt_start:txt_end, :img_seq_len] = 1.0 + + # 4. Allow unrestricted image self-attention + # This is the key difference from the restricted approach - all image tokens + # can attend to each other, which helps maintain global coherence across regions + regional_attention_mask[:img_seq_len, :img_seq_len] = 1.0 + + # Convert to boolean mask + regional_attention_mask = regional_attention_mask > 0.5 + + return regional_attention_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) + + # Resize mask to target dimensions + tf = torchvision.transforms.Resize( + (target_height, target_width), + interpolation=torchvision.transforms.InterpolationMode.NEAREST, + ) + + # Add batch dimension if needed: (h, w) -> (1, h, w) -> (1, 1, h, w) + if mask.ndim == 2: + mask = mask.unsqueeze(0) + if mask.ndim == 3: + mask = mask.unsqueeze(0) + + resized_mask = tf(mask) + + # Flatten to (1, 1, img_seq_len) + return resized_mask.flatten(start_dim=2).to(device=device) diff --git a/invokeai/backend/z_image/text_conditioning.py b/invokeai/backend/z_image/text_conditioning.py new file mode 100644 index 00000000000..5fe6933bba5 --- /dev/null +++ b/invokeai/backend/z_image/text_conditioning.py @@ -0,0 +1,74 @@ +from dataclasses import dataclass + +import torch + +from invokeai.backend.stable_diffusion.diffusion.conditioning_data import Range + + +@dataclass +class ZImageTextConditioning: + """Z-Image text conditioning with optional regional mask. + + Attributes: + prompt_embeds: Text embeddings from Qwen3 encoder. Shape: (seq_len, hidden_size). + 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). + """ + + prompt_embeds: torch.Tensor + mask: torch.Tensor | None = None + + +@dataclass +class ZImageRegionalTextConditioning: + """Container for multiple regional text conditionings concatenated together. + + In Z-Image, the unified sequence is [img_tokens, txt_tokens], which is different + from FLUX where it's [txt_tokens, img_tokens]. The attention mask must account for this. + + Attributes: + prompt_embeds: Concatenated text embeddings from all regional prompts. + Shape: (total_seq_len, hidden_size). + image_masks: List of binary masks for each regional prompt. + image_masks[i] corresponds to embedding_ranges[i]. + If None, the prompt is global (applies to entire image). + Shape: (1, 1, img_seq_len). + embedding_ranges: List of ranges indicating which portion of prompt_embeds + corresponds to each regional prompt. + """ + + prompt_embeds: torch.Tensor + image_masks: list[torch.Tensor | None] + embedding_ranges: list[Range] + + @classmethod + def from_text_conditionings( + cls, + text_conditionings: list[ZImageTextConditioning], + ) -> "ZImageRegionalTextConditioning": + """Create a ZImageRegionalTextConditioning from a list of ZImageTextConditioning objects. + + Args: + text_conditionings: List of text conditionings, each with optional mask. + + Returns: + A single ZImageRegionalTextConditioning with concatenated embeddings. + """ + concat_embeds: list[torch.Tensor] = [] + concat_ranges: list[Range] = [] + image_masks: list[torch.Tensor | None] = [] + + cur_embed_len = 0 + for tc in text_conditionings: + concat_embeds.append(tc.prompt_embeds) + concat_ranges.append(Range(start=cur_embed_len, end=cur_embed_len + tc.prompt_embeds.shape[0])) + image_masks.append(tc.mask) + cur_embed_len += tc.prompt_embeds.shape[0] + + prompt_embeds = torch.cat(concat_embeds, dim=0) + + return cls( + prompt_embeds=prompt_embeds, + image_masks=image_masks, + embedding_ranges=concat_ranges, + ) diff --git a/invokeai/backend/z_image/z_image_control_adapter.py b/invokeai/backend/z_image/z_image_control_adapter.py new file mode 100644 index 00000000000..e1efb0b9a45 --- /dev/null +++ b/invokeai/backend/z_image/z_image_control_adapter.py @@ -0,0 +1,238 @@ +# Adapted from https://github.com/aigc-apps/VideoX-Fun/blob/main/videox_fun/models/z_image_transformer2d_control.py +# Copyright (c) Alibaba, Inc. and its affiliates. +# Apache License 2.0 + +""" +Z-Image Control Adapter for InvokeAI. + +This module provides a standalone control adapter that can be combined with +a base ZImageTransformer2DModel at runtime. The adapter contains only the +control-specific layers (control_layers, control_all_x_embedder, control_noise_refiner). +""" + +from typing import List, Optional + +import torch +import torch.nn as nn +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.models.modeling_utils import ModelMixin +from diffusers.models.transformers.transformer_z_image import ( + SEQ_MULTI_OF, + ZImageTransformerBlock, +) +from torch.nn.utils.rnn import pad_sequence + + +class ZImageControlTransformerBlock(ZImageTransformerBlock): + """Control-specific transformer block with skip connections for hint generation.""" + + def __init__( + self, + layer_id: int, + dim: int, + n_heads: int, + n_kv_heads: int, + norm_eps: float, + qk_norm: bool, + modulation: bool = True, + block_id: int = 0, + ): + super().__init__(layer_id, dim, n_heads, n_kv_heads, norm_eps, qk_norm, modulation) + self.block_id = block_id + if block_id == 0: + self.before_proj = nn.Linear(dim, dim) + nn.init.zeros_(self.before_proj.weight) + nn.init.zeros_(self.before_proj.bias) + self.after_proj = nn.Linear(dim, dim) + nn.init.zeros_(self.after_proj.weight) + nn.init.zeros_(self.after_proj.bias) + + def forward( + self, + c: torch.Tensor, + x: torch.Tensor, + attn_mask: torch.Tensor, + freqs_cis: torch.Tensor, + adaln_input: Optional[torch.Tensor] = None, + ) -> torch.Tensor: + if self.block_id == 0: + c = self.before_proj(c) + x + all_c: list[torch.Tensor] = [] + else: + all_c = list(torch.unbind(c)) + c = all_c.pop(-1) + + c = super().forward(c, attn_mask=attn_mask, freqs_cis=freqs_cis, adaln_input=adaln_input) + c_skip = self.after_proj(c) + all_c += [c_skip, c] + c = torch.stack(all_c) + return c + + +class ZImageControlAdapter(ModelMixin, ConfigMixin): + """Standalone Z-Image Control Adapter. + + This adapter contains only the control-specific layers and can be combined + with a base ZImageTransformer2DModel at runtime. It computes control hints + that are added to the transformer's hidden states. + + The adapter supports 5 control modes: Canny, HED, Depth, Pose, MLSD. + Recommended control_context_scale: 0.65-0.80. + """ + + @register_to_config + def __init__( + self, + num_control_blocks: int = 6, # Number of control layer blocks + control_in_dim: int = 16, + all_patch_size: tuple[int, ...] = (2,), + all_f_patch_size: tuple[int, ...] = (1,), + dim: int = 3840, + n_refiner_layers: int = 2, + n_heads: int = 30, + n_kv_heads: int = 30, + norm_eps: float = 1e-5, + qk_norm: bool = True, + ): + super().__init__() + + self.dim = dim + self.control_in_dim = control_in_dim + self.all_patch_size = all_patch_size + self.all_f_patch_size = all_f_patch_size + + # Control patch embeddings + all_x_embedder = {} + for patch_size, f_patch_size in zip(all_patch_size, all_f_patch_size, strict=True): + x_embedder = nn.Linear( + f_patch_size * patch_size * patch_size * control_in_dim, + dim, + bias=True, + ) + all_x_embedder[f"{patch_size}-{f_patch_size}"] = x_embedder + + self.control_all_x_embedder = nn.ModuleDict(all_x_embedder) + + # Control noise refiner + self.control_noise_refiner = nn.ModuleList( + [ + ZImageTransformerBlock( + 1000 + layer_id, + dim, + n_heads, + n_kv_heads, + norm_eps, + qk_norm, + modulation=True, + ) + for layer_id in range(n_refiner_layers) + ] + ) + + # Control transformer blocks + self.control_layers = nn.ModuleList( + [ + ZImageControlTransformerBlock( + i, + dim, + n_heads, + n_kv_heads, + norm_eps, + qk_norm, + block_id=i, + ) + for i in range(num_control_blocks) + ] + ) + + # Padding token for control context + self.x_pad_token = nn.Parameter(torch.empty(dim)) + nn.init.normal_(self.x_pad_token, std=0.02) + + def forward( + self, + control_context: List[torch.Tensor], + unified_hidden_states: torch.Tensor, + cap_feats: torch.Tensor, + timestep_emb: torch.Tensor, + attn_mask: torch.Tensor, + freqs_cis: torch.Tensor, + rope_embedder, + patchify_fn, + patch_size: int = 2, + f_patch_size: int = 1, + ) -> tuple[torch.Tensor, ...]: + """Compute control hints from control context. + + Args: + control_context: List of control image latents [C, 1, H, W] + unified_hidden_states: Combined image+caption embeddings from main path + cap_feats: Caption feature embeddings + timestep_emb: Timestep embeddings + attn_mask: Attention mask + freqs_cis: RoPE frequencies + rope_embedder: RoPE embedder from base model + patchify_fn: Patchify function from base model + patch_size: Spatial patch size + f_patch_size: Frame patch size + + Returns: + Tuple of hint tensors to be added at each control layer position + """ + bsz = len(control_context) + device = control_context[0].device + + # Patchify control context using base model's patchify + ( + control_context_patches, + x_size, + x_pos_ids, + x_inner_pad_mask, + ) = patchify_fn(control_context, patch_size, f_patch_size, cap_feats.size(1)) + + # Embed control context + x_item_seqlens = [len(_) for _ in control_context_patches] + assert all(_ % SEQ_MULTI_OF == 0 for _ in x_item_seqlens) + x_max_item_seqlen = max(x_item_seqlens) + + control_context_cat = torch.cat(control_context_patches, dim=0) + control_context_cat = self.control_all_x_embedder[f"{patch_size}-{f_patch_size}"](control_context_cat) + + # Match timestep dtype + adaln_input = timestep_emb.type_as(control_context_cat) + control_context_cat[torch.cat(x_inner_pad_mask)] = self.x_pad_token + control_context_list = list(control_context_cat.split(x_item_seqlens, dim=0)) + x_freqs_cis = list(rope_embedder(torch.cat(x_pos_ids, dim=0)).split(x_item_seqlens, dim=0)) + + control_context_padded = pad_sequence(control_context_list, batch_first=True, padding_value=0.0) + x_freqs_cis = pad_sequence(x_freqs_cis, batch_first=True, padding_value=0.0) + x_attn_mask = torch.zeros((bsz, x_max_item_seqlen), dtype=torch.bool, device=device) + for i, seq_len in enumerate(x_item_seqlens): + x_attn_mask[i, :seq_len] = 1 + + # Refine control context + for layer in self.control_noise_refiner: + control_context_padded = layer(control_context_padded, x_attn_mask, x_freqs_cis, adaln_input) + + # Unify with caption features + cap_item_seqlens = [cap_feats.size(1)] * bsz + control_context_unified = [] + for i in range(bsz): + x_len = x_item_seqlens[i] + cap_len = cap_item_seqlens[i] + control_context_unified.append(torch.cat([control_context_padded[i][:x_len], cap_feats[i][:cap_len]])) + control_context_unified = pad_sequence(control_context_unified, batch_first=True, padding_value=0.0) + c = control_context_unified + + # Process through control layers + for layer in self.control_layers: + c = layer( + c, + x=unified_hidden_states, + attn_mask=attn_mask, + freqs_cis=freqs_cis, + adaln_input=adaln_input, + ) + + hints = torch.unbind(c)[:-1] + return hints diff --git a/invokeai/backend/z_image/z_image_control_transformer.py b/invokeai/backend/z_image/z_image_control_transformer.py new file mode 100644 index 00000000000..ab64c64582f --- /dev/null +++ b/invokeai/backend/z_image/z_image_control_transformer.py @@ -0,0 +1,643 @@ +# Adapted from https://github.com/aigc-apps/VideoX-Fun/blob/main/videox_fun/models/z_image_transformer2d_control.py +# Copyright (c) Alibaba, Inc. and its affiliates. +# Apache License 2.0 + +""" +Z-Image Control Transformer for InvokeAI. + +This module provides the ZImageControlTransformer2DModel which extends the base +ZImageTransformer2DModel with control conditioning capabilities (Canny, HED, Depth, Pose, MLSD). +""" + +from typing import Any, Dict, List, Optional + +import torch +import torch.nn as nn +from diffusers.configuration_utils import register_to_config +from diffusers.models.transformers.transformer_z_image import ( + SEQ_MULTI_OF, + ZImageTransformer2DModel, + ZImageTransformerBlock, +) +from diffusers.utils import is_torch_version +from torch.nn.utils.rnn import pad_sequence + + +class ZImageControlTransformerBlock(ZImageTransformerBlock): + """Control-specific transformer block with skip connections for hint generation. + + This block extends ZImageTransformerBlock with before_proj and after_proj layers + that create skip connections for the control signal. The hints are accumulated + across blocks and used to condition the main transformer. + """ + + def __init__( + self, + layer_id: int, + dim: int, + n_heads: int, + n_kv_heads: int, + norm_eps: float, + qk_norm: bool, + modulation: bool = True, + block_id: int = 0, + ): + super().__init__(layer_id, dim, n_heads, n_kv_heads, norm_eps, qk_norm, modulation) + self.block_id = block_id + if block_id == 0: + self.before_proj = nn.Linear(dim, dim) + nn.init.zeros_(self.before_proj.weight) + nn.init.zeros_(self.before_proj.bias) + self.after_proj = nn.Linear(dim, dim) + nn.init.zeros_(self.after_proj.weight) + nn.init.zeros_(self.after_proj.bias) + + def forward( + self, + c: torch.Tensor, + x: torch.Tensor, + attn_mask: torch.Tensor, + freqs_cis: torch.Tensor, + adaln_input: Optional[torch.Tensor] = None, + ) -> torch.Tensor: + if self.block_id == 0: + c = self.before_proj(c) + x + all_c: list[torch.Tensor] = [] + else: + all_c = list(torch.unbind(c)) + c = all_c.pop(-1) + + c = super().forward(c, attn_mask=attn_mask, freqs_cis=freqs_cis, adaln_input=adaln_input) + c_skip = self.after_proj(c) + all_c += [c_skip, c] + c = torch.stack(all_c) + return c + + +class BaseZImageTransformerBlock(ZImageTransformerBlock): + """Modified transformer block that accepts control hints. + + This block extends ZImageTransformerBlock to add control hints to the + hidden states at specific positions in the network. + """ + + def __init__( + self, + layer_id: int, + dim: int, + n_heads: int, + n_kv_heads: int, + norm_eps: float, + qk_norm: bool, + modulation: bool = True, + block_id: Optional[int] = 0, + ): + super().__init__(layer_id, dim, n_heads, n_kv_heads, norm_eps, qk_norm, modulation) + self.block_id = block_id + + def forward( + self, + hidden_states: torch.Tensor, + attn_mask: torch.Tensor, + freqs_cis: torch.Tensor, + adaln_input: Optional[torch.Tensor] = None, + hints: Optional[tuple[torch.Tensor, ...]] = None, + context_scale: float = 1.0, + ) -> torch.Tensor: + hidden_states = super().forward( + hidden_states, + attn_mask=attn_mask, + freqs_cis=freqs_cis, + adaln_input=adaln_input, + ) + if self.block_id is not None and hints is not None: + hidden_states = hidden_states + hints[self.block_id] * context_scale + return hidden_states + + +class ZImageControlTransformer2DModel(ZImageTransformer2DModel): + """Z-Image Control Transformer for spatial conditioning. + + This model extends ZImageTransformer2DModel with control layers that process + a control image (e.g., Canny edges, depth map) and inject control signals + into the main transformer at every other layer. + + The control model supports 5 modes: Canny, HED, Depth, Pose, MLSD. + Recommended control_context_scale: 0.65-0.80. + + Args: + control_layers_places: List of layer indices where control is applied. + Defaults to every other layer [0, 2, 4, ...]. + control_in_dim: Input dimension for control context. Defaults to in_channels. + All other args are passed to ZImageTransformer2DModel. + """ + + @register_to_config + def __init__( + self, + control_layers_places: Optional[List[int]] = None, + control_in_dim: Optional[int] = None, + all_patch_size: tuple[int, ...] = (2,), + all_f_patch_size: tuple[int, ...] = (1,), + in_channels: int = 16, + dim: int = 3840, + n_layers: int = 30, + n_refiner_layers: int = 2, + n_heads: int = 30, + n_kv_heads: int = 30, + norm_eps: float = 1e-5, + qk_norm: bool = True, + cap_feat_dim: int = 2560, + rope_theta: float = 256.0, + t_scale: float = 1000.0, + axes_dims: tuple[int, ...] = (32, 48, 48), + axes_lens: tuple[int, ...] = (1024, 512, 512), + ): + super().__init__( + all_patch_size=all_patch_size, + all_f_patch_size=all_f_patch_size, + in_channels=in_channels, + dim=dim, + n_layers=n_layers, + n_refiner_layers=n_refiner_layers, + n_heads=n_heads, + n_kv_heads=n_kv_heads, + norm_eps=norm_eps, + qk_norm=qk_norm, + cap_feat_dim=cap_feat_dim, + rope_theta=rope_theta, + t_scale=t_scale, + axes_dims=axes_dims, + axes_lens=axes_lens, + ) + + # Control layer configuration + self.control_layers_places = ( + list(range(0, n_layers, 2)) if control_layers_places is None else control_layers_places + ) + self.control_in_dim = in_channels if control_in_dim is None else control_in_dim + + assert 0 in self.control_layers_places + self.control_layers_mapping = {i: n for n, i in enumerate(self.control_layers_places)} + + # Replace standard layers with control-aware layers + del self.layers + self.layers = nn.ModuleList( + [ + BaseZImageTransformerBlock( + i, + dim, + n_heads, + n_kv_heads, + norm_eps, + qk_norm, + block_id=self.control_layers_mapping[i] if i in self.control_layers_places else None, + ) + for i in range(n_layers) + ] + ) + + # Control transformer blocks + self.control_layers = nn.ModuleList( + [ + ZImageControlTransformerBlock( + i, + dim, + n_heads, + n_kv_heads, + norm_eps, + qk_norm, + block_id=i, + ) + for i in range(len(self.control_layers_places)) + ] + ) + + # Control patch embeddings + all_x_embedder = {} + for patch_size, f_patch_size in zip(all_patch_size, all_f_patch_size, strict=True): + x_embedder = nn.Linear( + f_patch_size * patch_size * patch_size * self.control_in_dim, + dim, + bias=True, + ) + all_x_embedder[f"{patch_size}-{f_patch_size}"] = x_embedder + + self.control_all_x_embedder = nn.ModuleDict(all_x_embedder) + + # Control noise refiner + self.control_noise_refiner = nn.ModuleList( + [ + ZImageTransformerBlock( + 1000 + layer_id, + dim, + n_heads, + n_kv_heads, + norm_eps, + qk_norm, + modulation=True, + ) + for layer_id in range(n_refiner_layers) + ] + ) + + def patchify( + self, + all_image: List[torch.Tensor], + patch_size: int, + f_patch_size: int, + cap_seq_len: int, + ) -> tuple[List[torch.Tensor], List[tuple], List[torch.Tensor], List[torch.Tensor]]: + """Patchify images without embedding. + + This method extracts patches from images for control context processing. + Unlike patchify_and_embed, this only processes images without caption features. + + Args: + all_image: List of image tensors [C, F, H, W] + patch_size: Spatial patch size (height and width) + f_patch_size: Frame patch size + cap_seq_len: Caption sequence length (for position ID offset) + + Returns: + Tuple of: + - all_image_out: List of patchified image tensors + - all_image_size: List of (F, H, W) tuples + - all_image_pos_ids: List of position ID tensors + - all_image_pad_mask: List of padding mask tensors + """ + pH = pW = patch_size + pF = f_patch_size + device = all_image[0].device + + all_image_out = [] + all_image_size = [] + all_image_pos_ids = [] + all_image_pad_mask = [] + + # Calculate padded caption length for position offset + cap_padding_len = (-cap_seq_len) % SEQ_MULTI_OF + cap_padded_len = cap_seq_len + cap_padding_len + + for image in all_image: + C, F, H, W = image.size() + all_image_size.append((F, H, W)) + F_tokens, H_tokens, W_tokens = F // pF, H // pH, W // pW + + # Patchify: [C, F, H, W] -> [(F*H*W)/(patch), patch_elements * C] + image = image.view(C, F_tokens, pF, H_tokens, pH, W_tokens, pW) + image = image.permute(1, 3, 5, 2, 4, 6, 0).reshape(F_tokens * H_tokens * W_tokens, pF * pH * pW * C) + + image_ori_len = len(image) + image_padding_len = (-image_ori_len) % SEQ_MULTI_OF + + # Create position IDs + image_ori_pos_ids = self.create_coordinate_grid( + size=(F_tokens, H_tokens, W_tokens), + start=(cap_padded_len + 1, 0, 0), + device=device, + ).flatten(0, 2) + image_padding_pos_ids = ( + self.create_coordinate_grid( + size=(1, 1, 1), + start=(0, 0, 0), + device=device, + ) + .flatten(0, 2) + .repeat(image_padding_len, 1) + ) + image_padded_pos_ids = torch.cat([image_ori_pos_ids, image_padding_pos_ids], dim=0) + all_image_pos_ids.append(image_padded_pos_ids) + + # Padding mask + all_image_pad_mask.append( + torch.cat( + [ + torch.zeros((image_ori_len,), dtype=torch.bool, device=device), + torch.ones((image_padding_len,), dtype=torch.bool, device=device), + ], + dim=0, + ) + ) + + # Padded feature + image_padded_feat = torch.cat([image, image[-1:].repeat(image_padding_len, 1)], dim=0) + all_image_out.append(image_padded_feat) + + return all_image_out, all_image_size, all_image_pos_ids, all_image_pad_mask + + def forward_control( + self, + x: torch.Tensor, + cap_feats: torch.Tensor, + control_context: List[torch.Tensor], + kwargs: Dict[str, Any], + t: torch.Tensor, + patch_size: int = 2, + f_patch_size: int = 1, + ) -> tuple[torch.Tensor, ...]: + """Process control context and generate hints for the main transformer. + + Args: + x: Unified image+caption embeddings from main path + cap_feats: Caption feature embeddings + control_context: List of control images (VAE-encoded latents) + kwargs: Additional kwargs including attn_mask, freqs_cis + t: Timestep embeddings + patch_size: Spatial patch size + f_patch_size: Frame patch size + + Returns: + Tuple of hint tensors to be added at each control layer position + """ + bsz = len(control_context) + device = control_context[0].device + + # Patchify control context + ( + control_context_patches, + x_size, + x_pos_ids, + x_inner_pad_mask, + ) = self.patchify(control_context, patch_size, f_patch_size, cap_feats.size(1)) + + # Embed control context + x_item_seqlens = [len(_) for _ in control_context_patches] + assert all(_ % SEQ_MULTI_OF == 0 for _ in x_item_seqlens) + x_max_item_seqlen = max(x_item_seqlens) + + control_context_cat = torch.cat(control_context_patches, dim=0) + control_context_cat = self.control_all_x_embedder[f"{patch_size}-{f_patch_size}"](control_context_cat) + + # Match t_embedder output dtype + adaln_input = t.type_as(control_context_cat) + control_context_cat[torch.cat(x_inner_pad_mask)] = self.x_pad_token + control_context_list = list(control_context_cat.split(x_item_seqlens, dim=0)) + x_freqs_cis = list(self.rope_embedder(torch.cat(x_pos_ids, dim=0)).split(x_item_seqlens, dim=0)) + + control_context_padded = pad_sequence(control_context_list, batch_first=True, padding_value=0.0) + x_freqs_cis = pad_sequence(x_freqs_cis, batch_first=True, padding_value=0.0) + x_attn_mask = torch.zeros((bsz, x_max_item_seqlen), dtype=torch.bool, device=device) + for i, seq_len in enumerate(x_item_seqlens): + x_attn_mask[i, :seq_len] = 1 + + # Refine control context + if torch.is_grad_enabled() and self.gradient_checkpointing: + for layer in self.control_noise_refiner: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + ckpt_kwargs: Dict[str, Any] = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + control_context_padded = torch.utils.checkpoint.checkpoint( + create_custom_forward(layer), + control_context_padded, + x_attn_mask, + x_freqs_cis, + adaln_input, + **ckpt_kwargs, + ) + else: + for layer in self.control_noise_refiner: + control_context_padded = layer(control_context_padded, x_attn_mask, x_freqs_cis, adaln_input) + + # Unify with caption features + cap_item_seqlens = [cap_feats.size(1)] * bsz # Assume same length for batch + control_context_unified = [] + for i in range(bsz): + x_len = x_item_seqlens[i] + cap_len = cap_item_seqlens[i] + control_context_unified.append(torch.cat([control_context_padded[i][:x_len], cap_feats[i][:cap_len]])) + control_context_unified = pad_sequence(control_context_unified, batch_first=True, padding_value=0.0) + c = control_context_unified + + # Process through control layers + for layer in self.control_layers: + if torch.is_grad_enabled() and self.gradient_checkpointing: + + def create_custom_forward(module, **static_kwargs): + def custom_forward(*inputs): + return module(*inputs, **static_kwargs) + + return custom_forward + + ckpt_kwargs = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + c = torch.utils.checkpoint.checkpoint( + create_custom_forward( + layer, + x=x, + attn_mask=kwargs["attn_mask"], + freqs_cis=kwargs["freqs_cis"], + adaln_input=kwargs["adaln_input"], + ), + c, + **ckpt_kwargs, + ) + else: + c = layer( + c, + x=x, + attn_mask=kwargs["attn_mask"], + freqs_cis=kwargs["freqs_cis"], + adaln_input=kwargs["adaln_input"], + ) + + hints = torch.unbind(c)[:-1] + return hints + + def forward( + self, + x: List[torch.Tensor], + t: torch.Tensor, + cap_feats: List[torch.Tensor], + patch_size: int = 2, + f_patch_size: int = 1, + control_context: Optional[List[torch.Tensor]] = None, + control_context_scale: float = 1.0, + ) -> tuple[List[torch.Tensor], dict]: + """Forward pass with control conditioning. + + Args: + x: List of image tensors [B, C, 1, H, W] + t: Timestep tensor + cap_feats: List of caption feature tensors + patch_size: Spatial patch size (default 2) + f_patch_size: Frame patch size (default 1) + control_context: List of control image latents (VAE-encoded) + control_context_scale: Strength of control signal (0.65-0.80 recommended) + + Returns: + Tuple of (output tensors, empty dict) + """ + assert patch_size in self.all_patch_size + assert f_patch_size in self.all_f_patch_size + + if control_context is None: + # Fall back to base model behavior without control + return super().forward(x, t, cap_feats, patch_size, f_patch_size) + + bsz = len(x) + device = x[0].device + t = t * self.t_scale + t = self.t_embedder(t) + + ( + x, + cap_feats, + x_size, + x_pos_ids, + cap_pos_ids, + x_inner_pad_mask, + cap_inner_pad_mask, + ) = self.patchify_and_embed(x, cap_feats, patch_size, f_patch_size) + + # Image embedding and refinement + x_item_seqlens = [len(_) for _ in x] + assert all(_ % SEQ_MULTI_OF == 0 for _ in x_item_seqlens) + x_max_item_seqlen = max(x_item_seqlens) + + x = torch.cat(x, dim=0) + x = self.all_x_embedder[f"{patch_size}-{f_patch_size}"](x) + + adaln_input = t.type_as(x) + x[torch.cat(x_inner_pad_mask)] = self.x_pad_token + x = list(x.split(x_item_seqlens, dim=0)) + x_freqs_cis = list(self.rope_embedder(torch.cat(x_pos_ids, dim=0)).split(x_item_seqlens, dim=0)) + + x = pad_sequence(x, batch_first=True, padding_value=0.0) + x_freqs_cis = pad_sequence(x_freqs_cis, batch_first=True, padding_value=0.0) + x_attn_mask = torch.zeros((bsz, x_max_item_seqlen), dtype=torch.bool, device=device) + for i, seq_len in enumerate(x_item_seqlens): + x_attn_mask[i, :seq_len] = 1 + + # Noise refiner + if torch.is_grad_enabled() and self.gradient_checkpointing: + for layer in self.noise_refiner: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + ckpt_kwargs: Dict[str, Any] = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + x = torch.utils.checkpoint.checkpoint( + create_custom_forward(layer), + x, + x_attn_mask, + x_freqs_cis, + adaln_input, + **ckpt_kwargs, + ) + else: + for layer in self.noise_refiner: + x = layer(x, x_attn_mask, x_freqs_cis, adaln_input) + + # Caption embedding and refinement + cap_item_seqlens = [len(_) for _ in cap_feats] + assert all(_ % SEQ_MULTI_OF == 0 for _ in cap_item_seqlens) + cap_max_item_seqlen = max(cap_item_seqlens) + + cap_feats = torch.cat(cap_feats, dim=0) + cap_feats = self.cap_embedder(cap_feats) + cap_feats[torch.cat(cap_inner_pad_mask)] = self.cap_pad_token + cap_feats = list(cap_feats.split(cap_item_seqlens, dim=0)) + cap_freqs_cis = list(self.rope_embedder(torch.cat(cap_pos_ids, dim=0)).split(cap_item_seqlens, dim=0)) + + cap_feats = pad_sequence(cap_feats, batch_first=True, padding_value=0.0) + cap_freqs_cis = pad_sequence(cap_freqs_cis, batch_first=True, padding_value=0.0) + cap_attn_mask = torch.zeros((bsz, cap_max_item_seqlen), dtype=torch.bool, device=device) + for i, seq_len in enumerate(cap_item_seqlens): + cap_attn_mask[i, :seq_len] = 1 + + if torch.is_grad_enabled() and self.gradient_checkpointing: + for layer in self.context_refiner: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + ckpt_kwargs: Dict[str, Any] = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + cap_feats = torch.utils.checkpoint.checkpoint( + create_custom_forward(layer), + cap_feats, + cap_attn_mask, + cap_freqs_cis, + **ckpt_kwargs, + ) + else: + for layer in self.context_refiner: + cap_feats = layer(cap_feats, cap_attn_mask, cap_freqs_cis) + + # Unified processing + unified = [] + unified_freqs_cis = [] + for i in range(bsz): + x_len = x_item_seqlens[i] + cap_len = cap_item_seqlens[i] + unified.append(torch.cat([x[i][:x_len], cap_feats[i][:cap_len]])) + unified_freqs_cis.append(torch.cat([x_freqs_cis[i][:x_len], cap_freqs_cis[i][:cap_len]])) + unified_item_seqlens = [a + b for a, b in zip(cap_item_seqlens, x_item_seqlens, strict=True)] + unified_max_item_seqlen = max(unified_item_seqlens) + + unified = pad_sequence(unified, batch_first=True, padding_value=0.0) + unified_freqs_cis = pad_sequence(unified_freqs_cis, batch_first=True, padding_value=0.0) + unified_attn_mask = torch.zeros((bsz, unified_max_item_seqlen), dtype=torch.bool, device=device) + for i, seq_len in enumerate(unified_item_seqlens): + unified_attn_mask[i, :seq_len] = 1 + + # Generate control hints + kwargs = { + "attn_mask": unified_attn_mask, + "freqs_cis": unified_freqs_cis, + "adaln_input": adaln_input, + } + hints = self.forward_control( + unified, + cap_feats, + control_context, + kwargs, + t=t, + patch_size=patch_size, + f_patch_size=f_patch_size, + ) + + # Main transformer with control hints + for layer in self.layers: + layer_kwargs = { + "attn_mask": unified_attn_mask, + "freqs_cis": unified_freqs_cis, + "adaln_input": adaln_input, + "hints": hints, + "context_scale": control_context_scale, + } + if torch.is_grad_enabled() and self.gradient_checkpointing: + + def create_custom_forward(module, **static_kwargs): + def custom_forward(*inputs): + return module(*inputs, **static_kwargs) + + return custom_forward + + ckpt_kwargs = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + + unified = torch.utils.checkpoint.checkpoint( + create_custom_forward(layer, **layer_kwargs), + unified, + **ckpt_kwargs, + ) + else: + unified = layer(unified, **layer_kwargs) + + # Final layer and unpatchify + unified = self.all_final_layer[f"{patch_size}-{f_patch_size}"](unified, adaln_input) + unified = list(unified.unbind(dim=0)) + x = self.unpatchify(unified, x_size, patch_size, f_patch_size) + + x = torch.stack(x) + return x, {} diff --git a/invokeai/backend/z_image/z_image_controlnet_extension.py b/invokeai/backend/z_image/z_image_controlnet_extension.py new file mode 100644 index 00000000000..8eadf043fd0 --- /dev/null +++ b/invokeai/backend/z_image/z_image_controlnet_extension.py @@ -0,0 +1,531 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Z-Image ControlNet Extension for spatial conditioning. + +This module provides an extension-based approach to Z-Image ControlNet, +similar to how FLUX ControlNet works. Instead of duplicating the entire +transformer, we compute control hints separately and inject them into +the base transformer's forward pass. +""" + +from typing import List, Optional, Tuple + +import torch +from diffusers.models.transformers.transformer_z_image import ZImageTransformer2DModel +from torch.nn.utils.rnn import pad_sequence + +from invokeai.backend.z_image.z_image_control_adapter import ZImageControlAdapter +from invokeai.backend.z_image.z_image_patchify_utils import SEQ_MULTI_OF, patchify_control_context + + +class ZImageControlNetExtension: + """Extension for Z-Image ControlNet - computes control hints without duplicating the transformer. + + This class follows the same pattern as FLUX ControlNet extensions: + - The control adapter is loaded separately + - Control hints are computed per step + - Hints are injected into the transformer's layer outputs + + Attributes: + control_adapter: The Z-Image control adapter model + control_cond: VAE-encoded control image latents + weight: Control strength (recommended: 0.65-0.80) + begin_step_percent: When to start applying control (0.0 = start) + end_step_percent: When to stop applying control (1.0 = end) + """ + + def __init__( + self, + control_adapter: ZImageControlAdapter, + control_cond: torch.Tensor, + weight: float = 0.75, + begin_step_percent: float = 0.0, + end_step_percent: float = 1.0, + skip_layers: int = 0, # Skip first N control injection layers + ): + self._adapter = control_adapter + self._control_cond = control_cond + self._weight = weight + self._begin_step_percent = begin_step_percent + self._end_step_percent = end_step_percent + self._skip_layers = skip_layers + + # Get actual number of control blocks from loaded model (not config!) + # The safetensors may have more blocks than the config suggests + self._num_control_blocks = len(control_adapter.control_layers) + + # Control layers are applied at every other layer (0, 2, 4, ...) + # This matches the default configuration in the original implementation + self._control_places = [i * 2 for i in range(self._num_control_blocks)] + + # DEBUG: Print control configuration + print(f"[DEBUG] Actual num_control_blocks: {self._num_control_blocks}") + print(f"[DEBUG] control_places: {self._control_places}") + + # DEBUG: Check if control_layers have non-zero weights + first_layer = control_adapter.control_layers[0] + if hasattr(first_layer, "after_proj"): + after_proj_norm = first_layer.after_proj.weight.norm().item() + print(f"[DEBUG] First control layer after_proj weight norm: {after_proj_norm}") + if after_proj_norm < 1e-6: + print("[WARNING] after_proj weights are near-zero! Weights may not be loaded correctly.") + + @property + def weight(self) -> float: + return self._weight + + @property + def control_places(self) -> List[int]: + return self._control_places + + def should_apply(self, step_index: int, total_steps: int) -> bool: + """Check if control should be applied at this step.""" + if total_steps == 0: + return True + step_percent = step_index / total_steps + return self._begin_step_percent <= step_percent <= self._end_step_percent + + def prepare_control_state( + self, + base_transformer: ZImageTransformer2DModel, + cap_feats: torch.Tensor, + timestep_emb: torch.Tensor, + x_item_seqlens: List[int], + cap_item_seqlens: List[int], + x_freqs_cis: torch.Tensor, + patch_size: int = 2, + f_patch_size: int = 1, + ) -> torch.Tensor: + """Prepare control state (control_unified) for incremental hint computation. + + This processes the control condition through patchify and noise_refiner, + returning the control_unified tensor that will be used incrementally. + """ + bsz = 1 + device = self._control_cond.device + + # Patchify control context + control_context = [self._control_cond] + ( + control_patches, + _, + _control_pos_ids, + control_pad_mask, + ) = patchify_control_context( + control_context, + patch_size, + f_patch_size, + cap_feats.size(1), + ) + + # Embed control context + ctrl_item_seqlens = [len(p) for p in control_patches] + ctrl_max_seqlen = max(ctrl_item_seqlens) + + control_cat = torch.cat(control_patches, dim=0) + embedder_key = f"{patch_size}-{f_patch_size}" + control_cat = self._adapter.control_all_x_embedder[embedder_key](control_cat) + + # Apply padding token + adaln_input = timestep_emb.type_as(control_cat) + x_pad_token = self._adapter.x_pad_token.to(dtype=control_cat.dtype) + control_cat[torch.cat(control_pad_mask)] = x_pad_token + + control_list = list(control_cat.split(ctrl_item_seqlens, dim=0)) + control_padded = pad_sequence(control_list, batch_first=True, padding_value=0.0) + + # Use x_freqs_cis from main path for aligned position encoding + ctrl_freqs_cis_for_refiner = x_freqs_cis[:, : control_padded.shape[1]] + + ctrl_attn_mask = torch.zeros((bsz, ctrl_max_seqlen), dtype=torch.bool, device=device) + for i, seq_len in enumerate(ctrl_item_seqlens): + ctrl_attn_mask[i, :seq_len] = 1 + + # Refine control context through control_noise_refiner + for layer in self._adapter.control_noise_refiner: + control_padded = layer(control_padded, ctrl_attn_mask, ctrl_freqs_cis_for_refiner, adaln_input) + + # Store these for compute_single_hint + self._ctrl_item_seqlens = ctrl_item_seqlens + self._adaln_input = adaln_input + + # Unify control with caption features + control_unified = [] + for i in range(bsz): + ctrl_len = ctrl_item_seqlens[i] + cap_len = cap_item_seqlens[i] + control_unified.append(torch.cat([control_padded[i][:ctrl_len], cap_feats[i][:cap_len]])) + + control_unified = pad_sequence(control_unified, batch_first=True, padding_value=0.0) + + # DEBUG (only once) + if not hasattr(self, "_prepare_printed"): + self._prepare_printed = True + print(f"[DEBUG] Control state prepared: shape {control_unified.shape}") + + return control_unified + + def compute_single_hint( + self, + control_layer_idx: int, + control_state: torch.Tensor, + unified_hidden_states: torch.Tensor, + attn_mask: torch.Tensor, + freqs_cis: torch.Tensor, + adaln_input: torch.Tensor, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Compute a single hint from one control layer. + + Args: + control_layer_idx: Which control layer to use (0, 1, 2, ...) + control_state: Current control state (stacked tensor from previous layers) + unified_hidden_states: Current unified hidden states from main transformer + attn_mask: Attention mask + freqs_cis: RoPE frequencies + adaln_input: Timestep embedding + + Returns: + Tuple of (hint tensor, updated control_state) + """ + layer = self._adapter.control_layers[control_layer_idx] + + # Run control layer with CURRENT unified_hidden_states + control_state = layer( + control_state, + x=unified_hidden_states, + attn_mask=attn_mask, + freqs_cis=freqs_cis, + adaln_input=adaln_input, + ) + + # Extract hint from stacked state + # After control layer, control_state is stacked: [skip_0, ..., skip_n, running_state] + # We want the latest skip (second to last element) + unbinded = torch.unbind(control_state) + hint = unbinded[-2] # Latest skip connection + + return hint, control_state + + def compute_hints( + self, + base_transformer: ZImageTransformer2DModel, + unified_hidden_states: torch.Tensor, + cap_feats: torch.Tensor, + timestep_emb: torch.Tensor, + attn_mask: torch.Tensor, + freqs_cis: torch.Tensor, + x_item_seqlens: List[int], + cap_item_seqlens: List[int], + x_freqs_cis: torch.Tensor, + patch_size: int = 2, + f_patch_size: int = 1, + ) -> Tuple[torch.Tensor, ...]: + """Compute control hints using the adapter. + + This method processes the control condition through the adapter's + control_noise_refiner and control_layers to produce hints that + will be added to the transformer's hidden states. + + Args: + base_transformer: The base Z-Image transformer (for rope_embedder) + unified_hidden_states: Combined image+caption hidden states + cap_feats: Caption feature embeddings (padded) + timestep_emb: Timestep embeddings (adaln_input) + attn_mask: Unified attention mask + freqs_cis: RoPE frequencies + x_item_seqlens: Image sequence lengths per batch item + cap_item_seqlens: Caption sequence lengths per batch item + patch_size: Spatial patch size + f_patch_size: Frame patch size + + Returns: + Tuple of hint tensors to add at each control layer position + """ + # control_cond is always [C, F, H, W] format (single control image) + # where C = control_in_dim (16 for V1, 33 for V2.0), F = 1 frame + bsz = 1 + device = self._control_cond.device + + # Wrap control_cond in a list for patchify_control_context + # Expected input: List of [C, F, H, W] tensors + control_context = [self._control_cond] + + # Patchify control context + # Note: We don't use control_pos_ids anymore - we use x_freqs_cis from main path instead + ( + control_patches, + _, + _control_pos_ids, # Not used - we use main path's position encoding + control_pad_mask, + ) = patchify_control_context( + control_context, + patch_size, + f_patch_size, + cap_feats.size(1), + ) + + # Embed control context + ctrl_item_seqlens = [len(p) for p in control_patches] + assert all(s % SEQ_MULTI_OF == 0 for s in ctrl_item_seqlens) + ctrl_max_seqlen = max(ctrl_item_seqlens) + + control_cat = torch.cat(control_patches, dim=0) + embedder_key = f"{patch_size}-{f_patch_size}" + control_cat = self._adapter.control_all_x_embedder[embedder_key](control_cat) + + # Apply padding token (ensure dtype matches) + adaln_input = timestep_emb.type_as(control_cat) + x_pad_token = self._adapter.x_pad_token.to(dtype=control_cat.dtype) + control_cat[torch.cat(control_pad_mask)] = x_pad_token + + control_list = list(control_cat.split(ctrl_item_seqlens, dim=0)) + + control_padded = pad_sequence(control_list, batch_first=True, padding_value=0.0) + + # Use x_freqs_cis from main path for control patches (same spatial structure) + # This ensures control and image have aligned position encodings + ctrl_freqs_cis_for_refiner = x_freqs_cis[:, : control_padded.shape[1]] + + ctrl_attn_mask = torch.zeros((bsz, ctrl_max_seqlen), dtype=torch.bool, device=device) + for i, seq_len in enumerate(ctrl_item_seqlens): + ctrl_attn_mask[i, :seq_len] = 1 + + # Refine control context through control_noise_refiner + # Using x_freqs_cis to match main path's position encoding + for layer in self._adapter.control_noise_refiner: + control_padded = layer(control_padded, ctrl_attn_mask, ctrl_freqs_cis_for_refiner, adaln_input) + + # Unify control with caption features + control_unified = [] + for i in range(bsz): + ctrl_len = ctrl_item_seqlens[i] + cap_len = cap_item_seqlens[i] + control_unified.append(torch.cat([control_padded[i][:ctrl_len], cap_feats[i][:cap_len]])) + + control_unified = pad_sequence(control_unified, batch_first=True, padding_value=0.0) + c = control_unified + + # Process through control_layers to generate hints + # DEBUG: Print shapes before control_layers (only on first call) + if not hasattr(self, "_debug_printed"): + self._debug_printed = True + print(f"[DEBUG] control_unified shape: {control_unified.shape}") + print(f"[DEBUG] unified_hidden_states shape: {unified_hidden_states.shape}") + print(f"[DEBUG] ctrl_item_seqlens: {ctrl_item_seqlens}, x_item_seqlens: {x_item_seqlens}") + + # Check weight norms of critical layers + layer0 = self._adapter.control_layers[0] + if hasattr(layer0, "before_proj"): + print(f"[DEBUG] before_proj weight norm: {layer0.before_proj.weight.norm().item():.6f}") + if hasattr(layer0, "after_proj"): + print(f"[DEBUG] after_proj weight norm: {layer0.after_proj.weight.norm().item():.6f}") + + # Check control_noise_refiner weights + if len(self._adapter.control_noise_refiner) > 0: + refiner0 = self._adapter.control_noise_refiner[0] + if hasattr(refiner0, "attn"): + print(f"[DEBUG] noise_refiner[0] attn.wq norm: {refiner0.attn.wq.weight.norm().item():.6f}") + + for layer in self._adapter.control_layers: + c = layer( + c, + x=unified_hidden_states, + attn_mask=attn_mask, + freqs_cis=freqs_cis, + adaln_input=adaln_input, + ) + + # Extract hints (all but the last element which is the running state) + hints = tuple(torch.unbind(c)[:-1]) + + # DEBUG: Print hint shapes (only on first call) + if not hasattr(self, "_hints_printed"): + self._hints_printed = True + print(f"[DEBUG] Number of hints: {len(hints)}") + if hints: + print(f"[DEBUG] First hint shape: {hints[0].shape}") + # Also check hint statistics for each hint + for i, h in enumerate(hints[:3]): # First 3 hints + print( + f"[DEBUG] Hint[{i}] mean: {h.mean().item():.6f}, std: {h.std().item():.6f}, min: {h.min().item():.6f}, max: {h.max().item():.6f}" + ) + + return hints + + +def z_image_forward_with_control( + transformer: ZImageTransformer2DModel, + x: List[torch.Tensor], + t: torch.Tensor, + cap_feats: List[torch.Tensor], + control_extension: Optional[ZImageControlNetExtension] = None, + patch_size: int = 2, + f_patch_size: int = 1, +) -> Tuple[List[torch.Tensor], dict]: + """Forward pass through Z-Image transformer with optional control injection. + + This function replicates the base transformer's forward pass but allows + injecting control hints at specific layer positions. It uses the base + transformer's weights directly without duplicating them. + + Args: + transformer: The base Z-Image transformer model + x: List of image tensors [C, F, H, W] + t: Timestep tensor + cap_feats: List of caption feature tensors + control_extension: Optional control extension for hint injection + patch_size: Spatial patch size (default: 2) + f_patch_size: Frame patch size (default: 1) + + Returns: + Tuple of (output tensors list, empty dict for compatibility) + """ + assert patch_size in transformer.all_patch_size + assert f_patch_size in transformer.all_f_patch_size + + bsz = len(x) + device = x[0].device + t_scaled = t * transformer.t_scale + t_emb = transformer.t_embedder(t_scaled) + + # Patchify and embed using base transformer's method + ( + x_patches, + cap_feats_patches, + x_size, + x_pos_ids, + cap_pos_ids, + x_inner_pad_mask, + cap_inner_pad_mask, + ) = transformer.patchify_and_embed(x, cap_feats, patch_size, f_patch_size) + + # === X embed & refine === + x_item_seqlens = [len(p) for p in x_patches] + assert all(s % SEQ_MULTI_OF == 0 for s in x_item_seqlens) + x_max_item_seqlen = max(x_item_seqlens) + + embedder_key = f"{patch_size}-{f_patch_size}" + x_cat = torch.cat(x_patches, dim=0) + x_cat = transformer.all_x_embedder[embedder_key](x_cat) + + adaln_input = t_emb.type_as(x_cat) + x_cat[torch.cat(x_inner_pad_mask)] = transformer.x_pad_token + + x_list = list(x_cat.split(x_item_seqlens, dim=0)) + x_freqs_cis = list(transformer.rope_embedder(torch.cat(x_pos_ids, dim=0)).split([len(p) for p in x_pos_ids], dim=0)) + + x_padded = pad_sequence(x_list, batch_first=True, padding_value=0.0) + x_freqs_cis = pad_sequence(x_freqs_cis, batch_first=True, padding_value=0.0) + x_freqs_cis = x_freqs_cis[:, : x_padded.shape[1]] + + x_attn_mask = torch.zeros((bsz, x_max_item_seqlen), dtype=torch.bool, device=device) + for i, seq_len in enumerate(x_item_seqlens): + x_attn_mask[i, :seq_len] = 1 + + # Noise refiner + for layer in transformer.noise_refiner: + x_padded = layer(x_padded, x_attn_mask, x_freqs_cis, adaln_input) + + # === Cap embed & refine === + cap_item_seqlens = [len(p) for p in cap_feats_patches] + cap_max_item_seqlen = max(cap_item_seqlens) + + cap_cat = torch.cat(cap_feats_patches, dim=0) + cap_cat = transformer.cap_embedder(cap_cat) + cap_cat[torch.cat(cap_inner_pad_mask)] = transformer.cap_pad_token + + cap_list = list(cap_cat.split(cap_item_seqlens, dim=0)) + cap_freqs_cis = list( + transformer.rope_embedder(torch.cat(cap_pos_ids, dim=0)).split([len(p) for p in cap_pos_ids], dim=0) + ) + + cap_padded = pad_sequence(cap_list, batch_first=True, padding_value=0.0) + cap_freqs_cis = pad_sequence(cap_freqs_cis, batch_first=True, padding_value=0.0) + cap_freqs_cis = cap_freqs_cis[:, : cap_padded.shape[1]] + + cap_attn_mask = torch.zeros((bsz, cap_max_item_seqlen), dtype=torch.bool, device=device) + for i, seq_len in enumerate(cap_item_seqlens): + cap_attn_mask[i, :seq_len] = 1 + + # Context refiner + for layer in transformer.context_refiner: + cap_padded = layer(cap_padded, cap_attn_mask, cap_freqs_cis) + + # === Unified === + unified = [] + unified_freqs_cis = [] + for i in range(bsz): + x_len = x_item_seqlens[i] + cap_len = cap_item_seqlens[i] + unified.append(torch.cat([x_padded[i][:x_len], cap_padded[i][:cap_len]])) + unified_freqs_cis.append(torch.cat([x_freqs_cis[i][:x_len], cap_freqs_cis[i][:cap_len]])) + + unified_item_seqlens = [a + b for a, b in zip(cap_item_seqlens, x_item_seqlens, strict=False)] + unified_max_item_seqlen = max(unified_item_seqlens) + + unified = pad_sequence(unified, batch_first=True, padding_value=0.0) + unified_freqs_cis = pad_sequence(unified_freqs_cis, batch_first=True, padding_value=0.0) + + unified_attn_mask = torch.zeros((bsz, unified_max_item_seqlen), dtype=torch.bool, device=device) + for i, seq_len in enumerate(unified_item_seqlens): + unified_attn_mask[i, :seq_len] = 1 + + # === Compute control hints if extension provided === + # IMPORTANT: Hints are computed ONCE using the INITIAL unified state (before main layers) + # This matches the original VideoX-Fun architecture + control_places: List[int] = [] + control_weight: float = 1.0 + hints: Optional[Tuple[torch.Tensor, ...]] = None + + # DEBUG: Print number of transformer layers (only once per session) + if not hasattr(z_image_forward_with_control, "_layers_printed"): + z_image_forward_with_control._layers_printed = True + print(f"[DEBUG] Base transformer has {len(transformer.layers)} layers") + + if control_extension is not None: + # Compute ALL hints at once using the INITIAL unified state (before main layers run) + hints = control_extension.compute_hints( + base_transformer=transformer, + unified_hidden_states=unified, # INITIAL unified state! + cap_feats=cap_padded, + timestep_emb=adaln_input, + attn_mask=unified_attn_mask, + freqs_cis=unified_freqs_cis, + x_item_seqlens=x_item_seqlens, + cap_item_seqlens=cap_item_seqlens, + x_freqs_cis=x_freqs_cis, + patch_size=patch_size, + f_patch_size=f_patch_size, + ) + control_places = control_extension.control_places + control_weight = control_extension.weight + + # === Main transformer layers with pre-computed hint injection === + skip_layers = control_extension._skip_layers if control_extension is not None else 0 + control_layer_idx = 0 + for layer_idx, layer in enumerate(transformer.layers): + unified = layer(unified, unified_attn_mask, unified_freqs_cis, adaln_input) + + # Inject pre-computed control hint at designated positions + if hints is not None and layer_idx in control_places and control_layer_idx < len(hints): + # Skip first N hints if configured + if control_layer_idx >= skip_layers: + hint = hints[control_layer_idx] + + # DEBUG: Print on first injection + if not hasattr(z_image_forward_with_control, "_injection_printed"): + z_image_forward_with_control._injection_printed = True + print(f"[DEBUG] Injection at layer {layer_idx} (control_layer {control_layer_idx})") + print(f"[DEBUG] Hint mean: {hint.mean().item():.6f}, std: {hint.std().item():.6f}") + print(f"[DEBUG] Unified mean: {unified.mean().item():.6f}, std: {unified.std().item():.6f}") + print(f"[DEBUG] control_weight: {control_weight}, skip_layers: {skip_layers}") + + unified = unified + hint * control_weight + + control_layer_idx += 1 + + # === Final layer and unpatchify === + unified = transformer.all_final_layer[embedder_key](unified, adaln_input) + unified = list(unified.unbind(dim=0)) + output = transformer.unpatchify(unified, x_size, patch_size, f_patch_size) + + return output, {} diff --git a/invokeai/backend/z_image/z_image_patchify_utils.py b/invokeai/backend/z_image/z_image_patchify_utils.py new file mode 100644 index 00000000000..90472c8350e --- /dev/null +++ b/invokeai/backend/z_image/z_image_patchify_utils.py @@ -0,0 +1,135 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Utility functions for Z-Image patchify operations.""" + +from typing import List, Tuple + +import torch + +# Sequence must be multiple of this value (from diffusers transformer_z_image) +SEQ_MULTI_OF = 32 + + +def create_coordinate_grid( + size: Tuple[int, ...], + start: Tuple[int, ...] | None = None, + device: torch.device | None = None, +) -> torch.Tensor: + """Create a coordinate grid for position embeddings. + + Args: + size: Size of the grid (e.g., (F, H, W)) + start: Starting coordinates (default: all zeros) + device: Target device + + Returns: + Coordinate grid tensor of shape (*size, len(size)) + """ + if start is None: + start = tuple(0 for _ in size) + + axes = [ + torch.arange(x0, x0 + span, dtype=torch.int32, device=device) for x0, span in zip(start, size, strict=False) + ] + grids = torch.meshgrid(axes, indexing="ij") + return torch.stack(grids, dim=-1) + + +def patchify_control_context( + all_image: List[torch.Tensor], + patch_size: int, + f_patch_size: int, + cap_seq_len: int, +) -> Tuple[List[torch.Tensor], List[Tuple[int, int, int]], List[torch.Tensor], List[torch.Tensor]]: + """Patchify control images without embedding. + + This function extracts patches from control images for control context processing. + It handles padding and position ID creation for the control signal. + + Args: + all_image: List of control image tensors [C, F, H, W] + patch_size: Spatial patch size (height and width) + f_patch_size: Frame patch size + cap_seq_len: Caption sequence length (for position ID offset) + + Returns: + Tuple of: + - all_image_out: List of patchified image tensors + - all_image_size: List of (F, H, W) tuples + - all_image_pos_ids: List of position ID tensors + - all_image_pad_mask: List of padding mask tensors + """ + pH = pW = patch_size + pF = f_patch_size + device = all_image[0].device + + all_image_out: List[torch.Tensor] = [] + all_image_size: List[Tuple[int, int, int]] = [] + all_image_pos_ids: List[torch.Tensor] = [] + all_image_pad_mask: List[torch.Tensor] = [] + + # Calculate padded caption length for position offset + cap_padding_len = (-cap_seq_len) % SEQ_MULTI_OF + cap_padded_len = cap_seq_len + cap_padding_len + + for image in all_image: + C, F, H, W = image.size() + all_image_size.append((F, H, W)) + F_tokens, H_tokens, W_tokens = F // pF, H // pH, W // pW + + # Patchify: [C, F, H, W] -> [(F_tokens*H_tokens*W_tokens), (pF*pH*pW*C)] + # Step 1: Rearrange to put spatial dims together for proper patching + # [C, F, H, W] -> [F, H, W, C] + image = image.permute(1, 2, 3, 0).contiguous() + + # Step 2: Split H and W into tokens and patch sizes + # [F, H, W, C] -> [F, H_tokens, pH, W_tokens, pW, C] + image = image.view(F, H_tokens, pH, W_tokens, pW, C) + + # Step 3: Rearrange to group patches and features + # [F, H_tokens, pH, W_tokens, pW, C] -> [F, H_tokens, W_tokens, pH, pW, C] + image = image.permute(0, 1, 3, 2, 4, 5).contiguous() + + # Step 4: For F > 1, we'd need to handle F similarly, but for F=1 this is simpler + # Final reshape: [F*H_tokens*W_tokens, pH*pW*C] + num_patches = F_tokens * H_tokens * W_tokens + patch_features = pF * pH * pW * C + image = image.reshape(num_patches, patch_features) + + image_ori_len = len(image) + image_padding_len = (-image_ori_len) % SEQ_MULTI_OF + + # Create position IDs + image_ori_pos_ids = create_coordinate_grid( + size=(F_tokens, H_tokens, W_tokens), + start=(cap_padded_len + 1, 0, 0), + device=device, + ).flatten(0, 2) + + image_padding_pos_ids = ( + create_coordinate_grid( + size=(1, 1, 1), + start=(0, 0, 0), + device=device, + ) + .flatten(0, 2) + .repeat(image_padding_len, 1) + ) + image_padded_pos_ids = torch.cat([image_ori_pos_ids, image_padding_pos_ids], dim=0) + all_image_pos_ids.append(image_padded_pos_ids) + + # Padding mask + all_image_pad_mask.append( + torch.cat( + [ + torch.zeros((image_ori_len,), dtype=torch.bool, device=device), + torch.ones((image_padding_len,), dtype=torch.bool, device=device), + ], + dim=0, + ) + ) + + # Padded feature + image_padded_feat = torch.cat([image, image[-1:].repeat(image_padding_len, 1)], dim=0) + all_image_out.append(image_padded_feat) + + return all_image_out, all_image_size, all_image_pos_ids, all_image_pad_mask diff --git a/invokeai/backend/z_image/z_image_transformer_patch.py b/invokeai/backend/z_image/z_image_transformer_patch.py new file mode 100644 index 00000000000..6e42e678558 --- /dev/null +++ b/invokeai/backend/z_image/z_image_transformer_patch.py @@ -0,0 +1,234 @@ +"""Utilities for patching the ZImageTransformer2DModel to support regional attention masks.""" + +from contextlib import contextmanager +from typing import Callable, List, Optional, Tuple + +import torch +from torch.nn.utils.rnn import pad_sequence + + +def create_regional_forward( + original_forward: Callable, + regional_attn_mask: torch.Tensor, + img_seq_len: int, +) -> Callable: + """Create a modified forward function that uses a regional attention mask. + + The regional attention mask replaces the internally computed padding mask, + allowing for regional prompting where different image regions attend to + different text prompts. + + Args: + original_forward: The original forward method of ZImageTransformer2DModel. + regional_attn_mask: Attention mask of shape (seq_len, seq_len) where + seq_len = img_seq_len + txt_seq_len. + img_seq_len: Number of image tokens in the sequence. + + Returns: + A modified forward function with regional attention support. + """ + + def regional_forward( + self, + x: List[torch.Tensor], + t: torch.Tensor, + cap_feats: List[torch.Tensor], + patch_size: int = 2, + f_patch_size: int = 1, + ) -> Tuple[List[torch.Tensor], dict]: + """Modified forward with regional attention mask injection. + + This is based on the original ZImageTransformer2DModel.forward but + replaces the padding-based attention mask with a regional attention mask. + """ + assert patch_size in self.all_patch_size + assert f_patch_size in self.all_f_patch_size + + bsz = len(x) + device = x[0].device + t_scaled = t * self.t_scale + t_emb = self.t_embedder(t_scaled) + + SEQ_MULTI_OF = 32 # From diffusers transformer_z_image.py + + # Patchify and embed (reusing the original method) + ( + x, + cap_feats, + x_size, + x_pos_ids, + cap_pos_ids, + x_inner_pad_mask, + cap_inner_pad_mask, + ) = self.patchify_and_embed(x, cap_feats, patch_size, f_patch_size) + + # x embed & refine + x_item_seqlens = [len(_) for _ in x] + assert all(_ % SEQ_MULTI_OF == 0 for _ in x_item_seqlens) + x_max_item_seqlen = max(x_item_seqlens) + + x_cat = torch.cat(x, dim=0) + x_cat = self.all_x_embedder[f"{patch_size}-{f_patch_size}"](x_cat) + + adaln_input = t_emb.type_as(x_cat) + x_cat[torch.cat(x_inner_pad_mask)] = self.x_pad_token + x_list = list(x_cat.split(x_item_seqlens, dim=0)) + x_freqs_cis = list(self.rope_embedder(torch.cat(x_pos_ids, dim=0)).split(x_item_seqlens, dim=0)) + + x_padded = pad_sequence(x_list, batch_first=True, padding_value=0.0) + x_freqs_cis_padded = pad_sequence(x_freqs_cis, batch_first=True, padding_value=0.0) + x_attn_mask = torch.zeros((bsz, x_max_item_seqlen), dtype=torch.bool, device=device) + for i, seq_len in enumerate(x_item_seqlens): + x_attn_mask[i, :seq_len] = 1 + + # Process through noise_refiner + if torch.is_grad_enabled() and self.gradient_checkpointing: + for layer in self.noise_refiner: + x_padded = self._gradient_checkpointing_func( + layer, x_padded, x_attn_mask, x_freqs_cis_padded, adaln_input + ) + else: + for layer in self.noise_refiner: + x_padded = layer(x_padded, x_attn_mask, x_freqs_cis_padded, adaln_input) + + # cap embed & refine + cap_item_seqlens = [len(_) for _ in cap_feats] + assert all(_ % SEQ_MULTI_OF == 0 for _ in cap_item_seqlens) + cap_max_item_seqlen = max(cap_item_seqlens) + + cap_cat = torch.cat(cap_feats, dim=0) + cap_cat = self.cap_embedder(cap_cat) + cap_cat[torch.cat(cap_inner_pad_mask)] = self.cap_pad_token + cap_list = list(cap_cat.split(cap_item_seqlens, dim=0)) + cap_freqs_cis = list(self.rope_embedder(torch.cat(cap_pos_ids, dim=0)).split(cap_item_seqlens, dim=0)) + + cap_padded = pad_sequence(cap_list, batch_first=True, padding_value=0.0) + cap_freqs_cis_padded = pad_sequence(cap_freqs_cis, batch_first=True, padding_value=0.0) + cap_attn_mask = torch.zeros((bsz, cap_max_item_seqlen), dtype=torch.bool, device=device) + for i, seq_len in enumerate(cap_item_seqlens): + cap_attn_mask[i, :seq_len] = 1 + + # Process through context_refiner + if torch.is_grad_enabled() and self.gradient_checkpointing: + for layer in self.context_refiner: + cap_padded = self._gradient_checkpointing_func(layer, cap_padded, cap_attn_mask, cap_freqs_cis_padded) + else: + for layer in self.context_refiner: + cap_padded = layer(cap_padded, cap_attn_mask, cap_freqs_cis_padded) + + # Unified sequence: [img_tokens, txt_tokens] + unified = [] + unified_freqs_cis = [] + for i in range(bsz): + x_len = x_item_seqlens[i] + cap_len = cap_item_seqlens[i] + unified.append(torch.cat([x_padded[i][:x_len], cap_padded[i][:cap_len]])) + unified_freqs_cis.append(torch.cat([x_freqs_cis_padded[i][:x_len], cap_freqs_cis_padded[i][:cap_len]])) + + unified_item_seqlens = [a + b for a, b in zip(cap_item_seqlens, x_item_seqlens, strict=False)] + assert unified_item_seqlens == [len(_) for _ in unified] + unified_max_item_seqlen = max(unified_item_seqlens) + + unified_padded = pad_sequence(unified, batch_first=True, padding_value=0.0) + unified_freqs_cis_padded = pad_sequence(unified_freqs_cis, batch_first=True, padding_value=0.0) + + # --- REGIONAL ATTENTION MASK INJECTION --- + # Instead of using the padding mask, we use the regional attention mask + # The regional mask is (seq_len, seq_len), we need to expand it to (batch, seq_len, seq_len) + # and then add the batch dimension for broadcasting: (batch, 1, seq_len, seq_len) + + # Expand regional mask to match the actual sequence length (may include padding) + if regional_attn_mask.shape[0] != unified_max_item_seqlen: + # Pad the regional mask to match unified sequence length + padded_regional_mask = torch.zeros( + (unified_max_item_seqlen, unified_max_item_seqlen), + dtype=regional_attn_mask.dtype, + device=device, + ) + mask_size = min(regional_attn_mask.shape[0], unified_max_item_seqlen) + padded_regional_mask[:mask_size, :mask_size] = regional_attn_mask[:mask_size, :mask_size] + else: + padded_regional_mask = regional_attn_mask.to(device) + + # Convert boolean mask to additive float mask for attention + # True (attend) -> 0.0, False (block) -> -inf + # This is required because the attention backend expects additive masks for 4D inputs + # Use bfloat16 to match the transformer's query dtype + float_mask = torch.zeros_like(padded_regional_mask, dtype=torch.bfloat16) + float_mask[~padded_regional_mask] = float("-inf") + + # Expand to (batch, 1, seq_len, seq_len) for attention + unified_attn_mask = float_mask.unsqueeze(0).unsqueeze(0).expand(bsz, 1, -1, -1) + + # Process through main layers with regional attention mask + if torch.is_grad_enabled() and self.gradient_checkpointing: + for layer_idx, layer in enumerate(self.layers): + # Alternate between regional mask and full attention + if layer_idx % 2 == 0: + unified_padded = self._gradient_checkpointing_func( + layer, unified_padded, unified_attn_mask, unified_freqs_cis_padded, adaln_input + ) + else: + # Use padding mask only for odd layers (allows global coherence) + padding_mask = torch.zeros((bsz, unified_max_item_seqlen), dtype=torch.bool, device=device) + for i, seq_len in enumerate(unified_item_seqlens): + padding_mask[i, :seq_len] = 1 + unified_padded = self._gradient_checkpointing_func( + layer, unified_padded, padding_mask, unified_freqs_cis_padded, adaln_input + ) + else: + for layer_idx, layer in enumerate(self.layers): + # Alternate between regional mask and full attention + if layer_idx % 2 == 0: + unified_padded = layer(unified_padded, unified_attn_mask, unified_freqs_cis_padded, adaln_input) + else: + # Use padding mask only for odd layers (allows global coherence) + padding_mask = torch.zeros((bsz, unified_max_item_seqlen), dtype=torch.bool, device=device) + for i, seq_len in enumerate(unified_item_seqlens): + padding_mask[i, :seq_len] = 1 + unified_padded = layer(unified_padded, padding_mask, unified_freqs_cis_padded, adaln_input) + + # Final layer + unified_out = self.all_final_layer[f"{patch_size}-{f_patch_size}"](unified_padded, adaln_input) + unified_list = list(unified_out.unbind(dim=0)) + x_out = self.unpatchify(unified_list, x_size, patch_size, f_patch_size) + + return x_out, {} + + return regional_forward + + +@contextmanager +def patch_transformer_for_regional_prompting( + transformer, + regional_attn_mask: Optional[torch.Tensor], + img_seq_len: int, +): + """Context manager to temporarily patch the transformer for regional prompting. + + Args: + transformer: The ZImageTransformer2DModel instance. + regional_attn_mask: Regional attention mask of shape (seq_len, seq_len). + If None, the transformer is not patched. + img_seq_len: Number of image tokens. + + Yields: + The (possibly patched) transformer. + """ + if regional_attn_mask is None: + # No regional prompting, use original forward + yield transformer + return + + # Store original forward + original_forward = transformer.forward + + # Create and bind the regional forward + regional_fwd = create_regional_forward(original_forward, regional_attn_mask, img_seq_len) + transformer.forward = lambda *args, **kwargs: regional_fwd(transformer, *args, **kwargs) + + try: + yield transformer + finally: + # Restore original forward + transformer.forward = original_forward diff --git a/invokeai/configs/controlnet/cldm_v15.yaml b/invokeai/configs/controlnet/cldm_v15.yaml new file mode 100644 index 00000000000..fde1825577a --- /dev/null +++ b/invokeai/configs/controlnet/cldm_v15.yaml @@ -0,0 +1,79 @@ +model: + target: cldm.cldm.ControlLDM + 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" + control_key: "hint" + image_size: 64 + channels: 4 + cond_stage_trainable: false + conditioning_key: crossattn + monitor: val/loss_simple_ema + scale_factor: 0.18215 + use_ema: False + only_mid_control: False + + control_stage_config: + target: cldm.cldm.ControlNet + params: + image_size: 32 # unused + in_channels: 4 + hint_channels: 3 + 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 + + unet_config: + target: cldm.cldm.ControlledUnetModel + 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/invokeai/configs/controlnet/cldm_v21.yaml b/invokeai/configs/controlnet/cldm_v21.yaml new file mode 100644 index 00000000000..fc65193647e --- /dev/null +++ b/invokeai/configs/controlnet/cldm_v21.yaml @@ -0,0 +1,85 @@ +model: + target: cldm.cldm.ControlLDM + 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" + control_key: "hint" + image_size: 64 + channels: 4 + cond_stage_trainable: false + conditioning_key: crossattn + monitor: val/loss_simple_ema + scale_factor: 0.18215 + use_ema: False + only_mid_control: False + + control_stage_config: + target: cldm.cldm.ControlNet + params: + use_checkpoint: True + image_size: 32 # unused + in_channels: 4 + hint_channels: 3 + model_channels: 320 + attention_resolutions: [ 4, 2, 1 ] + num_res_blocks: 2 + channel_mult: [ 1, 2, 4, 4 ] + num_head_channels: 64 # need to fix for flash-attn + use_spatial_transformer: True + use_linear_in_transformer: True + transformer_depth: 1 + context_dim: 1024 + legacy: False + + unet_config: + target: cldm.cldm.ControlledUnetModel + params: + use_checkpoint: True + 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_head_channels: 64 # need to fix for flash-attn + use_spatial_transformer: True + use_linear_in_transformer: True + transformer_depth: 1 + context_dim: 1024 + legacy: False + + first_stage_config: + target: ldm.models.autoencoder.AutoencoderKL + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + #attn_type: "vanilla-xformers" + 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.FrozenOpenCLIPEmbedder + params: + freeze: True + layer: "penultimate" diff --git a/invokeai/configs/stable-diffusion/sd_xl_base.yaml b/invokeai/configs/stable-diffusion/sd_xl_base.yaml new file mode 100644 index 00000000000..2022dac9500 --- /dev/null +++ b/invokeai/configs/stable-diffusion/sd_xl_base.yaml @@ -0,0 +1,98 @@ +model: + target: sgm.models.diffusion.DiffusionEngine + params: + scale_factor: 0.13025 + disable_first_stage_autocast: True + + denoiser_config: + target: sgm.modules.diffusionmodules.denoiser.DiscreteDenoiser + params: + num_idx: 1000 + + weighting_config: + target: sgm.modules.diffusionmodules.denoiser_weighting.EpsWeighting + scaling_config: + target: sgm.modules.diffusionmodules.denoiser_scaling.EpsScaling + discretization_config: + target: sgm.modules.diffusionmodules.discretizer.LegacyDDPMDiscretization + + network_config: + target: sgm.modules.diffusionmodules.openaimodel.UNetModel + params: + adm_in_channels: 2816 + num_classes: sequential + use_checkpoint: True + in_channels: 4 + out_channels: 4 + model_channels: 320 + attention_resolutions: [4, 2] + num_res_blocks: 2 + channel_mult: [1, 2, 4] + num_head_channels: 64 + use_spatial_transformer: True + use_linear_in_transformer: True + transformer_depth: [1, 2, 10] # note: the first is unused (due to attn_res starting at 2) 32, 16, 8 --> 64, 32, 16 + context_dim: 2048 + spatial_transformer_attn_type: softmax-xformers + legacy: False + + conditioner_config: + target: sgm.modules.GeneralConditioner + params: + emb_models: + # crossattn cond + - is_trainable: False + input_key: txt + target: sgm.modules.encoders.modules.FrozenCLIPEmbedder + params: + layer: hidden + layer_idx: 11 + # crossattn and vector cond + - is_trainable: False + input_key: txt + target: sgm.modules.encoders.modules.FrozenOpenCLIPEmbedder2 + params: + arch: ViT-bigG-14 + version: laion2b_s39b_b160k + freeze: True + layer: penultimate + always_return_pooled: True + legacy: False + # vector cond + - is_trainable: False + input_key: original_size_as_tuple + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 # multiplied by two + # vector cond + - is_trainable: False + input_key: crop_coords_top_left + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 # multiplied by two + # vector cond + - is_trainable: False + input_key: target_size_as_tuple + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 # multiplied by two + + first_stage_config: + target: sgm.models.autoencoder.AutoencoderKLInferenceWrapper + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + attn_type: vanilla-xformers + 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 \ No newline at end of file diff --git a/invokeai/configs/stable-diffusion/sd_xl_inpaint.yaml b/invokeai/configs/stable-diffusion/sd_xl_inpaint.yaml new file mode 100644 index 00000000000..eea5c15a493 --- /dev/null +++ b/invokeai/configs/stable-diffusion/sd_xl_inpaint.yaml @@ -0,0 +1,98 @@ +model: + target: sgm.models.diffusion.DiffusionEngine + params: + scale_factor: 0.13025 + disable_first_stage_autocast: True + + denoiser_config: + target: sgm.modules.diffusionmodules.denoiser.DiscreteDenoiser + params: + num_idx: 1000 + + weighting_config: + target: sgm.modules.diffusionmodules.denoiser_weighting.EpsWeighting + scaling_config: + target: sgm.modules.diffusionmodules.denoiser_scaling.EpsScaling + discretization_config: + target: sgm.modules.diffusionmodules.discretizer.LegacyDDPMDiscretization + + network_config: + target: sgm.modules.diffusionmodules.openaimodel.UNetModel + params: + adm_in_channels: 2816 + num_classes: sequential + use_checkpoint: True + in_channels: 9 + out_channels: 4 + model_channels: 320 + attention_resolutions: [4, 2] + num_res_blocks: 2 + channel_mult: [1, 2, 4] + num_head_channels: 64 + use_spatial_transformer: True + use_linear_in_transformer: True + transformer_depth: [1, 2, 10] # note: the first is unused (due to attn_res starting at 2) 32, 16, 8 --> 64, 32, 16 + context_dim: 2048 + spatial_transformer_attn_type: softmax-xformers + legacy: False + + conditioner_config: + target: sgm.modules.GeneralConditioner + params: + emb_models: + # crossattn cond + - is_trainable: False + input_key: txt + target: sgm.modules.encoders.modules.FrozenCLIPEmbedder + params: + layer: hidden + layer_idx: 11 + # crossattn and vector cond + - is_trainable: False + input_key: txt + target: sgm.modules.encoders.modules.FrozenOpenCLIPEmbedder2 + params: + arch: ViT-bigG-14 + version: laion2b_s39b_b160k + freeze: True + layer: penultimate + always_return_pooled: True + legacy: False + # vector cond + - is_trainable: False + input_key: original_size_as_tuple + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 # multiplied by two + # vector cond + - is_trainable: False + input_key: crop_coords_top_left + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 # multiplied by two + # vector cond + - is_trainable: False + input_key: target_size_as_tuple + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 # multiplied by two + + first_stage_config: + target: sgm.models.autoencoder.AutoencoderKLInferenceWrapper + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + attn_type: vanilla-xformers + 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 \ No newline at end of file diff --git a/invokeai/configs/stable-diffusion/sd_xl_refiner.yaml b/invokeai/configs/stable-diffusion/sd_xl_refiner.yaml new file mode 100644 index 00000000000..cab5fe283d7 --- /dev/null +++ b/invokeai/configs/stable-diffusion/sd_xl_refiner.yaml @@ -0,0 +1,91 @@ +model: + target: sgm.models.diffusion.DiffusionEngine + params: + scale_factor: 0.13025 + disable_first_stage_autocast: True + + denoiser_config: + target: sgm.modules.diffusionmodules.denoiser.DiscreteDenoiser + params: + num_idx: 1000 + + weighting_config: + target: sgm.modules.diffusionmodules.denoiser_weighting.EpsWeighting + scaling_config: + target: sgm.modules.diffusionmodules.denoiser_scaling.EpsScaling + discretization_config: + target: sgm.modules.diffusionmodules.discretizer.LegacyDDPMDiscretization + + network_config: + target: sgm.modules.diffusionmodules.openaimodel.UNetModel + params: + adm_in_channels: 2560 + num_classes: sequential + use_checkpoint: True + in_channels: 4 + out_channels: 4 + model_channels: 384 + attention_resolutions: [4, 2] + num_res_blocks: 2 + channel_mult: [1, 2, 4, 4] + num_head_channels: 64 + use_spatial_transformer: True + use_linear_in_transformer: True + transformer_depth: 4 + context_dim: [1280, 1280, 1280, 1280] # 1280 + spatial_transformer_attn_type: softmax-xformers + legacy: False + + conditioner_config: + target: sgm.modules.GeneralConditioner + params: + emb_models: + # crossattn and vector cond + - is_trainable: False + input_key: txt + target: sgm.modules.encoders.modules.FrozenOpenCLIPEmbedder2 + params: + arch: ViT-bigG-14 + version: laion2b_s39b_b160k + legacy: False + freeze: True + layer: penultimate + always_return_pooled: True + # vector cond + - is_trainable: False + input_key: original_size_as_tuple + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 # multiplied by two + # vector cond + - is_trainable: False + input_key: crop_coords_top_left + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 # multiplied by two + # vector cond + - is_trainable: False + input_key: aesthetic_score + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 # multiplied by one + + first_stage_config: + target: sgm.models.autoencoder.AutoencoderKLInferenceWrapper + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + attn_type: vanilla-xformers + 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 diff --git a/configs/stable-diffusion/v1-finetune.yaml b/invokeai/configs/stable-diffusion/v1-finetune.yaml similarity index 78% rename from configs/stable-diffusion/v1-finetune.yaml rename to invokeai/configs/stable-diffusion/v1-finetune.yaml index 5d608811dee..8bbdb52159f 100644 --- a/configs/stable-diffusion/v1-finetune.yaml +++ b/invokeai/configs/stable-diffusion/v1-finetune.yaml @@ -1,6 +1,6 @@ model: base_learning_rate: 5.0e-03 - target: ldm.models.diffusion.ddpm.LatentDiffusion + target: invokeai.backend.stable_diffusion.diffusion.ddpm.LatentDiffusion params: linear_start: 0.00085 linear_end: 0.0120 @@ -19,7 +19,7 @@ model: embedding_reg_weight: 0.0 personalization_config: - target: ldm.modules.embedding_manager.EmbeddingManager + target: invokeai.backend.stable_diffusion.embedding_manager.EmbeddingManager params: placeholder_strings: ["*"] initializer_words: ["sculpture"] @@ -28,7 +28,7 @@ model: progressive_words: False unet_config: - target: ldm.modules.diffusionmodules.openaimodel.UNetModel + target: invokeai.backend.stable_diffusion.diffusionmodules.openaimodel.UNetModel params: image_size: 32 # unused in_channels: 4 @@ -45,7 +45,7 @@ model: legacy: False first_stage_config: - target: ldm.models.autoencoder.AutoencoderKL + target: invokeai.backend.stable_diffusion.autoencoder.AutoencoderKL params: embed_dim: 4 monitor: val/rec_loss @@ -68,7 +68,7 @@ model: target: torch.nn.Identity cond_stage_config: - target: ldm.modules.encoders.modules.FrozenCLIPEmbedder + target: invokeai.backend.stable_diffusion.encoders.modules.FrozenCLIPEmbedder data: target: main.DataModuleFromConfig @@ -77,14 +77,14 @@ data: num_workers: 2 wrap: false train: - target: ldm.data.personalized.PersonalizedBase + target: invokeai.backend.stable_diffusion.data.personalized.PersonalizedBase params: size: 512 set: train per_image_tokens: false repeats: 100 validation: - target: ldm.data.personalized.PersonalizedBase + target: invokeai.backend.stable_diffusion.data.personalized.PersonalizedBase params: size: 512 set: val @@ -105,5 +105,6 @@ lightning: trainer: benchmark: True - max_steps: 4000 - \ No newline at end of file + max_steps: 4000000 +# max_steps: 4000 + diff --git a/configs/stable-diffusion/v1-finetune_style.yaml b/invokeai/configs/stable-diffusion/v1-finetune_style.yaml similarity index 78% rename from configs/stable-diffusion/v1-finetune_style.yaml rename to invokeai/configs/stable-diffusion/v1-finetune_style.yaml index 1964d925e12..3442971a5bd 100644 --- a/configs/stable-diffusion/v1-finetune_style.yaml +++ b/invokeai/configs/stable-diffusion/v1-finetune_style.yaml @@ -1,6 +1,6 @@ model: base_learning_rate: 5.0e-03 - target: ldm.models.diffusion.ddpm.LatentDiffusion + target: invokeai.backend.models.diffusion.ddpm.LatentDiffusion params: linear_start: 0.00085 linear_end: 0.0120 @@ -19,7 +19,7 @@ model: embedding_reg_weight: 0.0 personalization_config: - target: ldm.modules.embedding_manager.EmbeddingManager + target: invokeai.backend.stable_diffusion.embedding_manager.EmbeddingManager params: placeholder_strings: ["*"] initializer_words: ["painting"] @@ -27,7 +27,7 @@ model: num_vectors_per_token: 1 unet_config: - target: ldm.modules.diffusionmodules.openaimodel.UNetModel + target: invokeai.backend.stable_diffusion.diffusionmodules.openaimodel.UNetModel params: image_size: 32 # unused in_channels: 4 @@ -44,7 +44,7 @@ model: legacy: False first_stage_config: - target: ldm.models.autoencoder.AutoencoderKL + target: invokeai.backend.stable_diffusion.autoencoder.AutoencoderKL params: embed_dim: 4 monitor: val/rec_loss @@ -67,7 +67,7 @@ model: target: torch.nn.Identity cond_stage_config: - target: ldm.modules.encoders.modules.FrozenCLIPEmbedder + target: invokeai.backend.stable_diffusion.encoders.modules.FrozenCLIPEmbedder data: target: main.DataModuleFromConfig @@ -76,14 +76,14 @@ data: num_workers: 16 wrap: false train: - target: ldm.data.personalized_style.PersonalizedBase + target: invokeai.backend.stable_diffusion.data.personalized_style.PersonalizedBase params: size: 512 set: train per_image_tokens: false repeats: 100 validation: - target: ldm.data.personalized_style.PersonalizedBase + target: invokeai.backend.stable_diffusion.data.personalized_style.PersonalizedBase params: size: 512 set: val diff --git a/invokeai/configs/stable-diffusion/v1-inference-v.yaml b/invokeai/configs/stable-diffusion/v1-inference-v.yaml new file mode 100644 index 00000000000..cb413c2567e --- /dev/null +++ b/invokeai/configs/stable-diffusion/v1-inference-v.yaml @@ -0,0 +1,80 @@ +model: + base_learning_rate: 1.0e-04 + target: invokeai.backend.models.diffusion.ddpm.LatentDiffusion + params: + parameterization: "v" + 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: invokeai.backend.stable_diffusion.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: invokeai.backend.stable_diffusion.embedding_manager.EmbeddingManager + params: + placeholder_strings: ["*"] + initializer_words: ['sculpture'] + per_image_tokens: false + num_vectors_per_token: 1 + progressive_words: False + + unet_config: + target: invokeai.backend.stable_diffusion.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: invokeai.backend.stable_diffusion.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: invokeai.backend.stable_diffusion.encoders.modules.WeightedFrozenCLIPEmbedder diff --git a/invokeai/configs/stable-diffusion/v1-inference.yaml b/invokeai/configs/stable-diffusion/v1-inference.yaml new file mode 100644 index 00000000000..7bcfe28f535 --- /dev/null +++ b/invokeai/configs/stable-diffusion/v1-inference.yaml @@ -0,0 +1,79 @@ +model: + base_learning_rate: 1.0e-04 + target: invokeai.backend.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: invokeai.backend.stable_diffusion.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: invokeai.backend.stable_diffusion.embedding_manager.EmbeddingManager + params: + placeholder_strings: ["*"] + initializer_words: ['sculpture'] + per_image_tokens: false + num_vectors_per_token: 1 + progressive_words: False + + unet_config: + target: invokeai.backend.stable_diffusion.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: invokeai.backend.stable_diffusion.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: invokeai.backend.stable_diffusion.encoders.modules.WeightedFrozenCLIPEmbedder diff --git a/invokeai/configs/stable-diffusion/v1-inpainting-inference.yaml b/invokeai/configs/stable-diffusion/v1-inpainting-inference.yaml new file mode 100644 index 00000000000..f6433cf97d2 --- /dev/null +++ b/invokeai/configs/stable-diffusion/v1-inpainting-inference.yaml @@ -0,0 +1,79 @@ +model: + base_learning_rate: 7.5e-05 + target: invokeai.backend.models.diffusion.ddpm.LatentInpaintDiffusion + 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: hybrid # important + monitor: val/loss_simple_ema + scale_factor: 0.18215 + finetune_keys: null + + scheduler_config: # 10000 warmup steps + target: invokeai.backend.stable_diffusion.lr_scheduler.LambdaLinearScheduler + params: + warm_up_steps: [ 2500 ] # NOTE for resuming. use 10000 if starting from scratch + cycle_lengths: [ 10000000000000 ] # incredibly large number to prevent corner cases + f_start: [ 1.e-6 ] + f_max: [ 1. ] + f_min: [ 1. ] + + personalization_config: + target: invokeai.backend.stable_diffusion.embedding_manager.EmbeddingManager + params: + placeholder_strings: ["*"] + initializer_words: ['sculpture'] + per_image_tokens: false + num_vectors_per_token: 8 + progressive_words: False + + unet_config: + target: invokeai.backend.stable_diffusion.diffusionmodules.openaimodel.UNetModel + params: + image_size: 32 # unused + in_channels: 9 # 4 data + 4 downscaled image + 1 mask + 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: invokeai.backend.stable_diffusion.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: invokeai.backend.stable_diffusion.encoders.modules.WeightedFrozenCLIPEmbedder diff --git a/invokeai/configs/stable-diffusion/v1-m1-finetune.yaml b/invokeai/configs/stable-diffusion/v1-m1-finetune.yaml new file mode 100644 index 00000000000..10255a9b70f --- /dev/null +++ b/invokeai/configs/stable-diffusion/v1-m1-finetune.yaml @@ -0,0 +1,110 @@ +model: + base_learning_rate: 5.0e-03 + target: invokeai.backend.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: image + cond_stage_key: caption + image_size: 64 + channels: 4 + cond_stage_trainable: true # Note: different from the one we trained before + conditioning_key: crossattn + monitor: val/loss_simple_ema + scale_factor: 0.18215 + use_ema: False + embedding_reg_weight: 0.0 + + personalization_config: + target: invokeai.backend.stable_diffusion.embedding_manager.EmbeddingManager + params: + placeholder_strings: ["*"] + initializer_words: ['sculpture'] + per_image_tokens: false + num_vectors_per_token: 6 + progressive_words: False + + unet_config: + target: invokeai.backend.stable_diffusion.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: invokeai.backend.stable_diffusion.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: invokeai.backend.stable_diffusion.encoders.modules.FrozenCLIPEmbedder + +data: + target: main.DataModuleFromConfig + params: + batch_size: 1 + num_workers: 2 + wrap: false + train: + target: invokeai.backend.stable_diffusion.data.personalized.PersonalizedBase + params: + size: 512 + set: train + per_image_tokens: false + repeats: 100 + validation: + target: invokeai.backend.stable_diffusion.data.personalized.PersonalizedBase + params: + size: 512 + set: val + per_image_tokens: false + repeats: 10 + +lightning: + modelcheckpoint: + params: + every_n_train_steps: 500 + callbacks: + image_logger: + target: main.ImageLogger + params: + batch_frequency: 500 + max_images: 5 + increase_log_steps: False + + trainer: + benchmark: False + max_steps: 6200 +# max_steps: 4000 + diff --git a/invokeai/configs/stable-diffusion/v2-inference-v.yaml b/invokeai/configs/stable-diffusion/v2-inference-v.yaml new file mode 100644 index 00000000000..0fe477d5e62 --- /dev/null +++ b/invokeai/configs/stable-diffusion/v2-inference-v.yaml @@ -0,0 +1,68 @@ +model: + base_learning_rate: 1.0e-4 + target: invokeai.backend.stable_diffusion.diffusion.ddpm.LatentDiffusion + params: + parameterization: "v" + 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 + conditioning_key: crossattn + monitor: val/loss_simple_ema + scale_factor: 0.18215 + use_ema: False # we set this to false because this is an inference only config + + unet_config: + target: invokeai.backend.stable_diffusion.diffusionmodules.openaimodel.UNetModel + params: + use_checkpoint: True + use_fp16: True + 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_head_channels: 64 # need to fix for flash-attn + use_spatial_transformer: True + use_linear_in_transformer: True + transformer_depth: 1 + context_dim: 1024 + legacy: False + + first_stage_config: + target: invokeai.backend.stable_diffusion.autoencoder.AutoencoderKL + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + #attn_type: "vanilla-xformers" + 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: invokeai.backend.stable_diffusion.encoders.modules.FrozenOpenCLIPEmbedder + params: + freeze: True + layer: "penultimate" diff --git a/invokeai/configs/stable-diffusion/v2-inference.yaml b/invokeai/configs/stable-diffusion/v2-inference.yaml new file mode 100644 index 00000000000..cde92ccdfea --- /dev/null +++ b/invokeai/configs/stable-diffusion/v2-inference.yaml @@ -0,0 +1,67 @@ +model: + base_learning_rate: 1.0e-4 + target: invokeai.backend.stable_diffusion.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 + conditioning_key: crossattn + monitor: val/loss_simple_ema + scale_factor: 0.18215 + use_ema: False # we set this to false because this is an inference only config + + unet_config: + target: invokeai.backend.stable_diffusion.diffusionmodules.openaimodel.UNetModel + params: + use_checkpoint: True + use_fp16: True + 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_head_channels: 64 # need to fix for flash-attn + use_spatial_transformer: True + use_linear_in_transformer: True + transformer_depth: 1 + context_dim: 1024 + legacy: False + + first_stage_config: + target: invokeai.backend.stable_diffusion.autoencoder.AutoencoderKL + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + #attn_type: "vanilla-xformers" + 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: invokeai.backend.stable_diffusion.encoders.modules.FrozenOpenCLIPEmbedder + params: + freeze: True + layer: "penultimate" diff --git a/invokeai/configs/stable-diffusion/v2-inpainting-inference-v.yaml b/invokeai/configs/stable-diffusion/v2-inpainting-inference-v.yaml new file mode 100644 index 00000000000..37cda460aac --- /dev/null +++ b/invokeai/configs/stable-diffusion/v2-inpainting-inference-v.yaml @@ -0,0 +1,159 @@ +model: + base_learning_rate: 5.0e-05 + target: ldm.models.diffusion.ddpm.LatentInpaintDiffusion + params: + linear_start: 0.00085 + linear_end: 0.0120 + parameterization: "v" + 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 + conditioning_key: hybrid + scale_factor: 0.18215 + monitor: val/loss_simple_ema + finetune_keys: null + use_ema: False + + unet_config: + target: ldm.modules.diffusionmodules.openaimodel.UNetModel + params: + use_checkpoint: True + image_size: 32 # unused + in_channels: 9 + out_channels: 4 + model_channels: 320 + attention_resolutions: [ 4, 2, 1 ] + num_res_blocks: 2 + channel_mult: [ 1, 2, 4, 4 ] + num_head_channels: 64 # need to fix for flash-attn + use_spatial_transformer: True + use_linear_in_transformer: True + transformer_depth: 1 + context_dim: 1024 + legacy: False + + first_stage_config: + target: ldm.models.autoencoder.AutoencoderKL + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + #attn_type: "vanilla-xformers" + 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.FrozenOpenCLIPEmbedder + params: + freeze: True + layer: "penultimate" + + +data: + target: ldm.data.laion.WebDataModuleFromConfig + params: + tar_base: null # for concat as in LAION-A + p_unsafe_threshold: 0.1 + filter_word_list: "data/filters.yaml" + max_pwatermark: 0.45 + batch_size: 8 + num_workers: 6 + multinode: True + min_size: 512 + train: + shards: + - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-0/{00000..18699}.tar -" + - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-1/{00000..18699}.tar -" + - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-2/{00000..18699}.tar -" + - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-3/{00000..18699}.tar -" + - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-4/{00000..18699}.tar -" #{00000-94333}.tar" + shuffle: 10000 + image_key: jpg + image_transforms: + - target: torchvision.transforms.Resize + params: + size: 512 + interpolation: 3 + - target: torchvision.transforms.RandomCrop + params: + size: 512 + postprocess: + target: ldm.data.laion.AddMask + params: + mode: "512train-large" + p_drop: 0.25 + # NOTE use enough shards to avoid empty validation loops in workers + validation: + shards: + - "pipe:aws s3 cp s3://deep-floyd-s3/datasets/laion_cleaned-part5/{93001..94333}.tar - " + shuffle: 0 + image_key: jpg + image_transforms: + - target: torchvision.transforms.Resize + params: + size: 512 + interpolation: 3 + - target: torchvision.transforms.CenterCrop + params: + size: 512 + postprocess: + target: ldm.data.laion.AddMask + params: + mode: "512train-large" + p_drop: 0.25 + +lightning: + find_unused_parameters: True + modelcheckpoint: + params: + every_n_train_steps: 5000 + + callbacks: + metrics_over_trainsteps_checkpoint: + params: + every_n_train_steps: 10000 + + image_logger: + target: main.ImageLogger + params: + enable_autocast: False + disabled: False + batch_frequency: 1000 + max_images: 4 + increase_log_steps: False + log_first_step: False + log_images_kwargs: + use_ema_scope: False + inpaint: False + plot_progressive_rows: False + plot_diffusion_rows: False + N: 4 + unconditional_guidance_scale: 5.0 + unconditional_guidance_label: [""] + ddim_steps: 50 # todo check these out for depth2img, + ddim_eta: 0.0 # todo check these out for depth2img, + + trainer: + benchmark: True + val_check_interval: 5000000 + num_sanity_val_steps: 0 + accumulate_grad_batches: 1 \ No newline at end of file diff --git a/invokeai/configs/stable-diffusion/v2-inpainting-inference.yaml b/invokeai/configs/stable-diffusion/v2-inpainting-inference.yaml new file mode 100644 index 00000000000..5aaf13162d4 --- /dev/null +++ b/invokeai/configs/stable-diffusion/v2-inpainting-inference.yaml @@ -0,0 +1,158 @@ +model: + base_learning_rate: 5.0e-05 + target: ldm.models.diffusion.ddpm.LatentInpaintDiffusion + 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 + conditioning_key: hybrid + scale_factor: 0.18215 + monitor: val/loss_simple_ema + finetune_keys: null + use_ema: False + + unet_config: + target: ldm.modules.diffusionmodules.openaimodel.UNetModel + params: + use_checkpoint: True + image_size: 32 # unused + in_channels: 9 + out_channels: 4 + model_channels: 320 + attention_resolutions: [ 4, 2, 1 ] + num_res_blocks: 2 + channel_mult: [ 1, 2, 4, 4 ] + num_head_channels: 64 # need to fix for flash-attn + use_spatial_transformer: True + use_linear_in_transformer: True + transformer_depth: 1 + context_dim: 1024 + legacy: False + + first_stage_config: + target: ldm.models.autoencoder.AutoencoderKL + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + #attn_type: "vanilla-xformers" + 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.FrozenOpenCLIPEmbedder + params: + freeze: True + layer: "penultimate" + + +data: + target: ldm.data.laion.WebDataModuleFromConfig + params: + tar_base: null # for concat as in LAION-A + p_unsafe_threshold: 0.1 + filter_word_list: "data/filters.yaml" + max_pwatermark: 0.45 + batch_size: 8 + num_workers: 6 + multinode: True + min_size: 512 + train: + shards: + - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-0/{00000..18699}.tar -" + - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-1/{00000..18699}.tar -" + - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-2/{00000..18699}.tar -" + - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-3/{00000..18699}.tar -" + - "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-4/{00000..18699}.tar -" #{00000-94333}.tar" + shuffle: 10000 + image_key: jpg + image_transforms: + - target: torchvision.transforms.Resize + params: + size: 512 + interpolation: 3 + - target: torchvision.transforms.RandomCrop + params: + size: 512 + postprocess: + target: ldm.data.laion.AddMask + params: + mode: "512train-large" + p_drop: 0.25 + # NOTE use enough shards to avoid empty validation loops in workers + validation: + shards: + - "pipe:aws s3 cp s3://deep-floyd-s3/datasets/laion_cleaned-part5/{93001..94333}.tar - " + shuffle: 0 + image_key: jpg + image_transforms: + - target: torchvision.transforms.Resize + params: + size: 512 + interpolation: 3 + - target: torchvision.transforms.CenterCrop + params: + size: 512 + postprocess: + target: ldm.data.laion.AddMask + params: + mode: "512train-large" + p_drop: 0.25 + +lightning: + find_unused_parameters: True + modelcheckpoint: + params: + every_n_train_steps: 5000 + + callbacks: + metrics_over_trainsteps_checkpoint: + params: + every_n_train_steps: 10000 + + image_logger: + target: main.ImageLogger + params: + enable_autocast: False + disabled: False + batch_frequency: 1000 + max_images: 4 + increase_log_steps: False + log_first_step: False + log_images_kwargs: + use_ema_scope: False + inpaint: False + plot_progressive_rows: False + plot_diffusion_rows: False + N: 4 + unconditional_guidance_scale: 5.0 + unconditional_guidance_label: [""] + ddim_steps: 50 # todo check these out for depth2img, + ddim_eta: 0.0 # todo check these out for depth2img, + + trainer: + benchmark: True + val_check_interval: 5000000 + num_sanity_val_steps: 0 + accumulate_grad_batches: 1 \ No newline at end of file diff --git a/invokeai/configs/stable-diffusion/v2-midas-inference.yaml b/invokeai/configs/stable-diffusion/v2-midas-inference.yaml new file mode 100644 index 00000000000..f20c30f618b --- /dev/null +++ b/invokeai/configs/stable-diffusion/v2-midas-inference.yaml @@ -0,0 +1,74 @@ +model: + base_learning_rate: 5.0e-07 + target: ldm.models.diffusion.ddpm.LatentDepth2ImageDiffusion + 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 + conditioning_key: hybrid + scale_factor: 0.18215 + monitor: val/loss_simple_ema + finetune_keys: null + use_ema: False + + depth_stage_config: + target: ldm.modules.midas.api.MiDaSInference + params: + model_type: "dpt_hybrid" + + unet_config: + target: ldm.modules.diffusionmodules.openaimodel.UNetModel + params: + use_checkpoint: True + image_size: 32 # unused + in_channels: 5 + out_channels: 4 + model_channels: 320 + attention_resolutions: [ 4, 2, 1 ] + num_res_blocks: 2 + channel_mult: [ 1, 2, 4, 4 ] + num_head_channels: 64 # need to fix for flash-attn + use_spatial_transformer: True + use_linear_in_transformer: True + transformer_depth: 1 + context_dim: 1024 + legacy: False + + first_stage_config: + target: ldm.models.autoencoder.AutoencoderKL + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + #attn_type: "vanilla-xformers" + 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.FrozenOpenCLIPEmbedder + params: + freeze: True + layer: "penultimate" + + diff --git a/invokeai/frontend/__init__.py b/invokeai/frontend/__init__.py new file mode 100644 index 00000000000..19eafe46c44 --- /dev/null +++ b/invokeai/frontend/__init__.py @@ -0,0 +1,3 @@ +""" +Initialization file for invokeai.frontend +""" diff --git a/invokeai/frontend/cli/__init__.py b/invokeai/frontend/cli/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/invokeai/frontend/cli/arg_parser.py b/invokeai/frontend/cli/arg_parser.py new file mode 100644 index 00000000000..72da8f76560 --- /dev/null +++ b/invokeai/frontend/cli/arg_parser.py @@ -0,0 +1,46 @@ +from argparse import ArgumentParser, Namespace, RawTextHelpFormatter +from typing import Optional + +from invokeai.version import __version__ + +_root_help = r"""Path to the runtime root directory. If omitted, the app will search for the root directory in the following order: +- The `$INVOKEAI_ROOT` environment variable +- The currently active virtual environment's parent directory +- `$HOME/invokeai`""" + +_config_file_help = r"""Path to the invokeai.yaml configuration file. If omitted, the app will search for the file in the root directory.""" + +_parser = ArgumentParser(description="Invoke Studio", formatter_class=RawTextHelpFormatter) +_parser.add_argument("--root", type=str, help=_root_help) +_parser.add_argument("--config", dest="config_file", type=str, help=_config_file_help) +_parser.add_argument("--version", action="version", version=__version__, help="Displays the version and exits.") + + +class InvokeAIArgs: + """Helper class for parsing CLI args. + + Args should never be parsed within the application code, only in the CLI entrypoints. Parsing args within the + application creates conflicts when running tests or when using application modules directly. + + If the args are needed within the application, the consumer should access them from this class. + + Example: + ``` + # In a CLI wrapper + from invokeai.frontend.cli.arg_parser import InvokeAIArgs + InvokeAIArgs.parse_args() + + # In the application + from invokeai.frontend.cli.arg_parser import InvokeAIArgs + args = InvokeAIArgs.args + """ + + args: Optional[Namespace] = None + did_parse: bool = False + + @staticmethod + def parse_args() -> Optional[Namespace]: + """Parse CLI args and store the result.""" + InvokeAIArgs.args = _parser.parse_args() + InvokeAIArgs.did_parse = True + return InvokeAIArgs.args diff --git a/invokeai/frontend/install/__init__.py b/invokeai/frontend/install/__init__.py new file mode 100644 index 00000000000..2a248eb49f4 --- /dev/null +++ b/invokeai/frontend/install/__init__.py @@ -0,0 +1,3 @@ +""" +Initialization file for invokeai.frontend.config +""" diff --git a/invokeai/frontend/install/import_images.py b/invokeai/frontend/install/import_images.py new file mode 100644 index 00000000000..c08f379f593 --- /dev/null +++ b/invokeai/frontend/install/import_images.py @@ -0,0 +1,786 @@ +# Copyright (c) 2023 - The InvokeAI Team +# Primary Author: David Lovell (github @f412design, discord @techjedi) +# co-author, minor tweaks - Lincoln Stein + +# pylint: disable=line-too-long +# pylint: disable=broad-exception-caught +"""Script to import images into the new database system for 3.0.0""" + +import datetime +import glob +import json +import locale +import os +import re +import shutil +import sqlite3 +from pathlib import Path + +import PIL +import PIL.ImageOps +import PIL.PngImagePlugin +import yaml +from prompt_toolkit import prompt +from prompt_toolkit.completion import PathCompleter +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.shortcuts import message_dialog + +from invokeai.app.services.config.config_default import get_config +from invokeai.app.util.misc import uuid_string + +app_config = get_config() + +bindings = KeyBindings() + + +@bindings.add("c-c") +def _(event): + raise KeyboardInterrupt + + +# release notes +# "Use All" with size dimensions not selectable in the UI will not load dimensions + + +class Config: + """Configuration loader.""" + + def __init__(self): + pass + + TIMESTAMP_STRING = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") + + INVOKE_DIRNAME = "invokeai" + YAML_FILENAME = "invokeai.yaml" + DATABASE_FILENAME = "invokeai.db" + + database_path = None + database_backup_dir = None + outputs_path = None + thumbnail_path = None + + def find_and_load(self): + """Find the yaml config file and load""" + root = app_config.root_path + if not self.confirm_and_load(os.path.abspath(root)): + print("\r\nSpecify custom database and outputs paths:") + self.confirm_and_load_from_user() + + self.database_backup_dir = os.path.join(os.path.dirname(self.database_path), "backup") + self.thumbnail_path = os.path.join(self.outputs_path, "thumbnails") + + def confirm_and_load(self, invoke_root): + """Validate a yaml path exists, confirms the user wants to use it and loads config.""" + yaml_path = os.path.join(invoke_root, self.YAML_FILENAME) + if os.path.exists(yaml_path): + db_dir, outdir = self.load_paths_from_yaml(yaml_path) + if os.path.isabs(db_dir): + database_path = os.path.join(db_dir, self.DATABASE_FILENAME) + else: + database_path = os.path.join(invoke_root, db_dir, self.DATABASE_FILENAME) + + if os.path.isabs(outdir): + outputs_path = os.path.join(outdir, "images") + else: + outputs_path = os.path.join(invoke_root, outdir, "images") + + db_exists = os.path.exists(database_path) + outdir_exists = os.path.exists(outputs_path) + + text = f"Found {self.YAML_FILENAME} file at {yaml_path}:" + text += f"\n Database : {database_path}" + text += f"\n Outputs : {outputs_path}" + text += "\n\nUse these paths for import (yes) or choose different ones (no) [Yn]: " + + if db_exists and outdir_exists: + if (prompt(text).strip() or "Y").upper().startswith("Y"): + self.database_path = database_path + self.outputs_path = outputs_path + return True + else: + return False + else: + print(" Invalid: One or more paths in this config did not exist and cannot be used.") + + else: + message_dialog( + title="Path not found", + text=f"Auto-discovery of configuration failed! Could not find ({yaml_path}), Custom paths can be specified.", + ).run() + return False + + def confirm_and_load_from_user(self): + default = "" + while True: + database_path = os.path.expanduser( + prompt( + "Database: Specify absolute path to the database to import into: ", + completer=PathCompleter( + expanduser=True, file_filter=lambda x: Path(x).is_dir() or x.endswith((".db")) + ), + default=default, + ) + ) + if database_path.endswith(".db") and os.path.isabs(database_path) and os.path.exists(database_path): + break + default = database_path + "/" if Path(database_path).is_dir() else database_path + + default = "" + while True: + outputs_path = os.path.expanduser( + prompt( + "Outputs: Specify absolute path to outputs/images directory to import into: ", + completer=PathCompleter(expanduser=True, only_directories=True), + default=default, + ) + ) + + if outputs_path.endswith("images") and os.path.isabs(outputs_path) and os.path.exists(outputs_path): + break + default = outputs_path + "/" if Path(outputs_path).is_dir() else outputs_path + + self.database_path = database_path + self.outputs_path = outputs_path + + return + + def load_paths_from_yaml(self, yaml_path): + """Load an Invoke AI yaml file and get the database and outputs paths.""" + try: + with open(yaml_path, "rt", encoding=locale.getpreferredencoding()) as file: + yamlinfo = yaml.safe_load(file) + db_dir = yamlinfo.get("InvokeAI", {}).get("Paths", {}).get("db_dir", None) + outdir = yamlinfo.get("InvokeAI", {}).get("Paths", {}).get("outdir", None) + return db_dir, outdir + except Exception: + print(f"Failed to load paths from yaml file! {yaml_path}!") + return None, None + + +class ImportStats: + """DTO for tracking work progress.""" + + def __init__(self): + pass + + time_start = datetime.datetime.utcnow() + count_source_files = 0 + count_skipped_file_exists = 0 + count_skipped_db_exists = 0 + count_imported = 0 + count_imported_by_version = {} + count_file_errors = 0 + + @staticmethod + def get_elapsed_time_string(): + """Get a friendly time string for the time elapsed since processing start.""" + time_now = datetime.datetime.utcnow() + total_seconds = (time_now - ImportStats.time_start).total_seconds() + hours = int((total_seconds) / 3600) + minutes = int(((total_seconds) % 3600) / 60) + seconds = total_seconds % 60 + out_str = f"{hours} hour(s) -" if hours > 0 else "" + out_str += f"{minutes} minute(s) -" if minutes > 0 else "" + out_str += f"{seconds:.2f} second(s)" + return out_str + + +class InvokeAIMetadata: + """DTO for core Invoke AI generation properties parsed from metadata.""" + + def __init__(self): + pass + + def __str__(self): + formatted_str = f"{self.generation_mode}~{self.steps}~{self.cfg_scale}~{self.model_name}~{self.scheduler}~{self.seed}~{self.width}~{self.height}~{self.rand_device}~{self.strength}~{self.init_image}" + formatted_str += f"\r\npositive_prompt: {self.positive_prompt}" + formatted_str += f"\r\nnegative_prompt: {self.negative_prompt}" + return formatted_str + + generation_mode = None + steps = None + cfg_scale = None + model_name = None + scheduler = None + seed = None + width = None + height = None + rand_device = None + strength = None + init_image = None + positive_prompt = None + negative_prompt = None + imported_app_version = None + + def to_json(self): + """Convert the active instance to json format.""" + prop_dict = {} + prop_dict["generation_mode"] = self.generation_mode + # dont render prompt nodes if neither are set to avoid the ui thinking it can set them + # if at least one exists, render them both, but use empty string instead of None if one of them is empty + # this allows the field that is empty to actually be cleared byt he UI instead of leaving the previous value + if self.positive_prompt or self.negative_prompt: + prop_dict["positive_prompt"] = "" if self.positive_prompt is None else self.positive_prompt + prop_dict["negative_prompt"] = "" if self.negative_prompt is None else self.negative_prompt + prop_dict["width"] = self.width + prop_dict["height"] = self.height + # only render seed if it has a value to avoid ui thinking it can set this and then error + if self.seed: + prop_dict["seed"] = self.seed + prop_dict["rand_device"] = self.rand_device + prop_dict["cfg_scale"] = self.cfg_scale + prop_dict["steps"] = self.steps + prop_dict["scheduler"] = self.scheduler + prop_dict["clip_skip"] = 0 + prop_dict["model"] = {} + prop_dict["model"]["model_name"] = self.model_name + prop_dict["model"]["base_model"] = None + prop_dict["controlnets"] = [] + prop_dict["loras"] = [] + prop_dict["vae"] = None + prop_dict["strength"] = self.strength + prop_dict["init_image"] = self.init_image + prop_dict["positive_style_prompt"] = None + prop_dict["negative_style_prompt"] = None + prop_dict["refiner_model"] = None + prop_dict["refiner_cfg_scale"] = None + prop_dict["refiner_steps"] = None + prop_dict["refiner_scheduler"] = None + prop_dict["refiner_aesthetic_store"] = None + prop_dict["refiner_start"] = None + prop_dict["imported_app_version"] = self.imported_app_version + + return json.dumps(prop_dict) + + +class InvokeAIMetadataParser: + """Parses strings with json data to find Invoke AI core metadata properties.""" + + def __init__(self): + pass + + def parse_meta_tag_dream(self, dream_string): + """Take as input an png metadata json node for the 'dream' field variant from prior to 1.15""" + props = InvokeAIMetadata() + + props.imported_app_version = "pre1.15" + seed_match = re.search("-S\\s*(\\d+)", dream_string) + if seed_match is not None: + try: + props.seed = int(seed_match[1]) + except ValueError: + props.seed = None + raw_prompt = re.sub("(-S\\s*\\d+)", "", dream_string) + else: + raw_prompt = dream_string + + pos_prompt, neg_prompt = self.split_prompt(raw_prompt) + + props.positive_prompt = pos_prompt + props.negative_prompt = neg_prompt + + return props + + def parse_meta_tag_sd_metadata(self, tag_value): + """Take as input an png metadata json node for the 'sd-metadata' field variant from 1.15 through 2.3.5 post 2""" + props = InvokeAIMetadata() + + props.imported_app_version = tag_value.get("app_version") + props.model_name = tag_value.get("model_weights") + img_node = tag_value.get("image") + if img_node is not None: + props.generation_mode = img_node.get("type") + props.width = img_node.get("width") + props.height = img_node.get("height") + props.seed = img_node.get("seed") + props.rand_device = "cuda" # hardcoded since all generations pre 3.0 used cuda random noise instead of cpu + props.cfg_scale = img_node.get("cfg_scale") + props.steps = img_node.get("steps") + props.scheduler = self.map_scheduler(img_node.get("sampler")) + props.strength = img_node.get("strength") + if props.strength is None: + props.strength = img_node.get("strength_steps") # try second name for this property + props.init_image = img_node.get("init_image_path") + if props.init_image is None: # try second name for this property + props.init_image = img_node.get("init_img") + # remove the path info from init_image so if we move the init image, it will be correctly relative in the new location + if props.init_image is not None: + props.init_image = os.path.basename(props.init_image) + raw_prompt = img_node.get("prompt") + if isinstance(raw_prompt, list): + raw_prompt = raw_prompt[0].get("prompt") + + props.positive_prompt, props.negative_prompt = self.split_prompt(raw_prompt) + + return props + + def parse_meta_tag_invokeai(self, tag_value): + """Take as input an png metadata json node for the 'invokeai' field variant from 3.0.0 beta 1 through 5""" + props = InvokeAIMetadata() + + props.imported_app_version = "3.0.0 or later" + props.generation_mode = tag_value.get("type") + if props.generation_mode is not None: + props.generation_mode = props.generation_mode.replace("t2l", "txt2img").replace("l2l", "img2img") + + props.width = tag_value.get("width") + props.height = tag_value.get("height") + props.seed = tag_value.get("seed") + props.cfg_scale = tag_value.get("cfg_scale") + props.steps = tag_value.get("steps") + props.scheduler = tag_value.get("scheduler") + props.strength = tag_value.get("strength") + props.positive_prompt = tag_value.get("positive_conditioning") + props.negative_prompt = tag_value.get("negative_conditioning") + + return props + + def map_scheduler(self, old_scheduler): + """Convert the legacy sampler names to matching 3.0 schedulers""" + + # this was more elegant as a case statement, but that's not available in python 3.9 + if old_scheduler is None: + return None + scheduler_map = { + "ddim": "ddim", + "plms": "pnmd", + "k_lms": "lms", + "k_dpm_2": "kdpm_2", + "k_dpm_2_a": "kdpm_2_a", + "dpmpp_2": "dpmpp_2s", + "k_dpmpp_2": "dpmpp_2m", + "k_dpmpp_2_a": None, # invalid, in 2.3.x, selecting this sample would just fallback to last run or plms if new session + "k_euler": "euler", + "k_euler_a": "euler_a", + "k_heun": "heun", + } + return scheduler_map.get(old_scheduler) + + def split_prompt(self, raw_prompt: str): + """Split the unified prompt strings by extracting all negative prompt blocks out into the negative prompt.""" + if raw_prompt is None: + return "", "" + raw_prompt_search = raw_prompt.replace("\r", "").replace("\n", "") + matches = re.findall(r"\[(.+?)\]", raw_prompt_search) + if len(matches) > 0: + negative_prompt = "" + if len(matches) == 1: + negative_prompt = matches[0].strip().strip(",") + else: + for match in matches: + negative_prompt += f"({match.strip().strip(',')})" + positive_prompt = re.sub(r"(\[.+?\])", "", raw_prompt_search).strip() + else: + positive_prompt = raw_prompt_search.strip() + negative_prompt = "" + + return positive_prompt, negative_prompt + + +class DatabaseMapper: + """Class to abstract database functionality.""" + + def __init__(self, database_path, database_backup_dir): + self.database_path = database_path + self.database_backup_dir = database_backup_dir + self.connection = None + self.cursor = None + + def connect(self): + """Open connection to the database.""" + self.connection = sqlite3.connect(self.database_path) + self.cursor = self.connection.cursor() + + def get_board_names(self): + """Get a list of the current board names from the database.""" + sql_get_board_name = "SELECT board_name FROM boards" + self.cursor.execute(sql_get_board_name) + rows = self.cursor.fetchall() + return [row[0] for row in rows] + + def does_image_exist(self, image_name): + """Check database if a image name already exists and return a boolean.""" + sql_get_image_by_name = f"SELECT image_name FROM images WHERE image_name='{image_name}'" + self.cursor.execute(sql_get_image_by_name) + rows = self.cursor.fetchall() + return True if len(rows) > 0 else False + + def add_new_image_to_database(self, filename, width, height, metadata, modified_date_string): + """Add an image to the database.""" + sql_add_image = f"""INSERT INTO images (image_name, image_origin, image_category, width, height, session_id, node_id, metadata, is_intermediate, created_at, updated_at) +VALUES ('{filename}', 'internal', 'general', {width}, {height}, null, null, '{metadata}', 0, '{modified_date_string}', '{modified_date_string}')""" + self.cursor.execute(sql_add_image) + self.connection.commit() + + def get_board_id_with_create(self, board_name): + """Get the board id for supplied name, and create the board if one does not exist.""" + sql_find_board = f"SELECT board_id FROM boards WHERE board_name='{board_name}' COLLATE NOCASE" + self.cursor.execute(sql_find_board) + rows = self.cursor.fetchall() + if len(rows) > 0: + return rows[0][0] + else: + board_date_string = datetime.datetime.utcnow().date().isoformat() + new_board_id = uuid_string() + sql_insert_board = f"INSERT INTO boards (board_id, board_name, created_at, updated_at) VALUES ('{new_board_id}', '{board_name}', '{board_date_string}', '{board_date_string}')" + self.cursor.execute(sql_insert_board) + self.connection.commit() + return new_board_id + + def add_image_to_board(self, filename, board_id): + """Add an image mapping to a board.""" + add_datetime_str = datetime.datetime.utcnow().isoformat() + sql_add_image_to_board = f"""INSERT INTO board_images (board_id, image_name, created_at, updated_at) + VALUES ('{board_id}', '{filename}', '{add_datetime_str}', '{add_datetime_str}')""" + self.cursor.execute(sql_add_image_to_board) + self.connection.commit() + + def disconnect(self): + """Disconnect from the db, cleaning up connections and cursors.""" + if self.cursor is not None: + self.cursor.close() + if self.connection is not None: + self.connection.close() + + def backup(self, timestamp_string): + """Take a backup of the database.""" + if not os.path.exists(self.database_backup_dir): + print(f"Database backup directory {self.database_backup_dir} does not exist -> creating...", end="") + os.makedirs(self.database_backup_dir) + print("Done!") + database_backup_path = os.path.join(self.database_backup_dir, f"backup-{timestamp_string}-invokeai.db") + print(f"Making DB Backup at {database_backup_path}...", end="") + shutil.copy2(self.database_path, database_backup_path) + print("Done!") + + +class MediaImportProcessor: + """Containing class for script functionality.""" + + def __init__(self): + pass + + board_name_id_map = {} + + def get_import_file_list(self): + """Ask the user for the import folder and scan for the list of files to return.""" + while True: + default = "" + while True: + import_dir = os.path.expanduser( + prompt( + "Inputs: Specify absolute path containing InvokeAI .png images to import: ", + completer=PathCompleter(expanduser=True, only_directories=True), + default=default, + ) + ) + if len(import_dir) > 0 and Path(import_dir).is_dir(): + break + default = import_dir + + recurse_directories = ( + (prompt("Include files from subfolders recursively [yN]? ").strip() or "N").upper().startswith("N") + ) + if recurse_directories: + is_recurse = False + matching_file_list = glob.glob(import_dir + "/*.png", recursive=False) + else: + is_recurse = True + matching_file_list = glob.glob(import_dir + "/**/*.png", recursive=True) + + if len(matching_file_list) > 0: + return import_dir, is_recurse, matching_file_list + else: + print(f"The specific path {import_dir} exists, but does not contain .png files!") + + def get_file_details(self, filepath): + """Retrieve the embedded metedata fields and dimensions from an image file.""" + with PIL.Image.open(filepath) as img: + img.load() + png_width, png_height = img.size + img_info = img.info + return img_info, png_width, png_height + + def select_board_option(self, board_names, timestamp_string): + """Allow the user to choose how a board is selected for imported files.""" + while True: + print("\r\nOptions for board selection for imported images:") + print(f"1) Select an existing board name. (found {len(board_names)})") + print("2) Specify a board name to create/add to.") + print("3) Create/add to board named 'IMPORT'.") + print( + f"4) Create/add to board named 'IMPORT' with the current datetime string appended (.e.g IMPORT_{timestamp_string})." + ) + print( + "5) Create/add to board named 'IMPORT' with a the original file app_version appended (.e.g IMPORT_2.2.5)." + ) + input_option = input("Specify desired board option: ") + # This was more elegant as a case statement, but not supported in python 3.9 + if input_option == "1": + if len(board_names) < 1: + print("\r\nThere are no existing board names to choose from. Select another option!") + continue + board_name = self.select_item_from_list( + board_names, "board name", True, "Cancel, go back and choose a different board option." + ) + if board_name is not None: + return board_name + elif input_option == "2": + while True: + board_name = input("Specify new/existing board name: ") + if board_name: + return board_name + elif input_option == "3": + return "IMPORT" + elif input_option == "4": + return f"IMPORT_{timestamp_string}" + elif input_option == "5": + return "IMPORT_APPVERSION" + + def select_item_from_list(self, items, entity_name, allow_cancel, cancel_string): + """A general function to render a list of items to select in the console, prompt the user for a selection and ensure a valid entry is selected.""" + print(f"Select a {entity_name.lower()} from the following list:") + index = 1 + for item in items: + print(f"{index}) {item}") + index += 1 + if allow_cancel: + print(f"{index}) {cancel_string}") + while True: + try: + option_number = int(input("Specify number of selection: ")) + except ValueError: + continue + if allow_cancel and option_number == index: + return None + if option_number >= 1 and option_number <= len(items): + return items[option_number - 1] + + def import_image(self, filepath: str, board_name_option: str, db_mapper: DatabaseMapper, config: Config): + """Import a single file by its path""" + parser = InvokeAIMetadataParser() + file_name = os.path.basename(filepath) + file_destination_path = os.path.join(config.outputs_path, file_name) + + print("===============================================================================") + print(f"Importing {filepath}") + + # check destination to see if the file was previously imported + if os.path.exists(file_destination_path): + print("File already exists in the destination, skipping!") + ImportStats.count_skipped_file_exists += 1 + return + + # check if file name is already referenced in the database + if db_mapper.does_image_exist(file_name): + print("A reference to a file with this name already exists in the database, skipping!") + ImportStats.count_skipped_db_exists += 1 + return + + # load image info and dimensions + img_info, png_width, png_height = self.get_file_details(filepath) + + # parse metadata + destination_needs_meta_update = True + log_version_note = "(Unknown)" + if "invokeai_metadata" in img_info: + # for the latest, we will just re-emit the same json, no need to parse/modify + converted_field = None + latest_json_string = img_info.get("invokeai_metadata") + log_version_note = "3.0.0+" + destination_needs_meta_update = False + else: + if "sd-metadata" in img_info: + converted_field = parser.parse_meta_tag_sd_metadata(json.loads(img_info.get("sd-metadata"))) + elif "invokeai" in img_info: + converted_field = parser.parse_meta_tag_invokeai(json.loads(img_info.get("invokeai"))) + elif "dream" in img_info: + converted_field = parser.parse_meta_tag_dream(img_info.get("dream")) + elif "Dream" in img_info: + converted_field = parser.parse_meta_tag_dream(img_info.get("Dream")) + else: + converted_field = InvokeAIMetadata() + destination_needs_meta_update = False + print("File does not have metadata from known Invoke AI versions, add only, no update!") + + # use the loaded img dimensions if the metadata didnt have them + if converted_field.width is None: + converted_field.width = png_width + if converted_field.height is None: + converted_field.height = png_height + + log_version_note = converted_field.imported_app_version if converted_field else "NoVersion" + log_version_note = log_version_note or "NoVersion" + + latest_json_string = converted_field.to_json() + + print(f"From Invoke AI Version {log_version_note} with dimensions {png_width} x {png_height}.") + + # if metadata needs update, then update metdata and copy in one shot + if destination_needs_meta_update: + print("Updating metadata while copying...", end="") + self.update_file_metadata_while_copying( + filepath, file_destination_path, "invokeai_metadata", latest_json_string + ) + print("Done!") + else: + print("No metadata update necessary, copying only...", end="") + shutil.copy2(filepath, file_destination_path) + print("Done!") + + # create thumbnail + print("Creating thumbnail...", end="") + thumbnail_path = os.path.join(config.thumbnail_path, os.path.splitext(file_name)[0]) + ".webp" + thumbnail_size = 256, 256 + with PIL.Image.open(filepath) as source_image: + source_image.thumbnail(thumbnail_size) + source_image.save(thumbnail_path, "webp") + print("Done!") + + # finalize the dynamic board name if there is an APPVERSION token in it. + if converted_field is not None: + board_name = board_name_option.replace("APPVERSION", converted_field.imported_app_version or "NoVersion") + else: + board_name = board_name_option.replace("APPVERSION", "Latest") + + # maintain a map of alrady created/looked up ids to avoid DB queries + print("Finding/Creating board...", end="") + if board_name in self.board_name_id_map: + board_id = self.board_name_id_map[board_name] + else: + board_id = db_mapper.get_board_id_with_create(board_name) + self.board_name_id_map[board_name] = board_id + print("Done!") + + # add image to db + print("Adding image to database......", end="") + modified_time = datetime.datetime.utcfromtimestamp(os.path.getmtime(filepath)) + db_mapper.add_new_image_to_database(file_name, png_width, png_height, latest_json_string, modified_time) + print("Done!") + + # add image to board + print("Adding image to board......", end="") + db_mapper.add_image_to_board(file_name, board_id) + print("Done!") + + ImportStats.count_imported += 1 + if log_version_note in ImportStats.count_imported_by_version: + ImportStats.count_imported_by_version[log_version_note] += 1 + else: + ImportStats.count_imported_by_version[log_version_note] = 1 + + def update_file_metadata_while_copying(self, filepath, file_destination_path, tag_name, tag_value): + """Perform a metadata update with save to a new destination which accomplishes a copy while updating metadata.""" + with PIL.Image.open(filepath) as target_image: + existing_img_info = target_image.info + metadata = PIL.PngImagePlugin.PngInfo() + # re-add any existing invoke ai tags unless they are the one we are trying to add + for key in existing_img_info: + if key != tag_name and key in ("dream", "Dream", "sd-metadata", "invokeai", "invokeai_metadata"): + metadata.add_text(key, existing_img_info[key]) + metadata.add_text(tag_name, tag_value) + target_image.save(file_destination_path, pnginfo=metadata) + + def process(self): + """Begin main processing.""" + + print("===============================================================================") + print("This script will import images generated by earlier versions of") + print("InvokeAI into the currently installed root directory:") + print(f" {app_config.root_path}") + print("If this is not what you want to do, type ctrl-C now to cancel.") + + # load config + print("===============================================================================") + print("= Configuration & Settings") + + config = Config() + config.find_and_load() + db_mapper = DatabaseMapper(config.database_path, config.database_backup_dir) + db_mapper.connect() + + import_dir, is_recurse, import_file_list = self.get_import_file_list() + ImportStats.count_source_files = len(import_file_list) + + board_names = db_mapper.get_board_names() + board_name_option = self.select_board_option(board_names, config.TIMESTAMP_STRING) + + print("\r\n===============================================================================") + print("= Import Settings Confirmation") + + print() + print(f"Database File Path : {config.database_path}") + print(f"Outputs/Images Directory : {config.outputs_path}") + print(f"Import Image Source Directory : {import_dir}") + print(f" Recurse Source SubDirectories : {'Yes' if is_recurse else 'No'}") + print(f"Count of .png file(s) found : {len(import_file_list)}") + print(f"Board name option specified : {board_name_option}") + print(f"Database backup will be taken at : {config.database_backup_dir}") + + print("\r\nNotes about the import process:") + print("- Source image files will not be modified, only copied to the outputs directory.") + print("- If the same file name already exists in the destination, the file will be skipped.") + print("- If the same file name already has a record in the database, the file will be skipped.") + print("- Invoke AI metadata tags will be updated/written into the imported copy only.") + print( + "- On the imported copy, only Invoke AI known tags (latest and legacy) will be retained (dream, sd-metadata, invokeai, invokeai_metadata)" + ) + print( + "- A property 'imported_app_version' will be added to metadata that can be viewed in the UI's metadata viewer." + ) + print( + "- The new 3.x InvokeAI outputs folder structure is flat so recursively found source imges will all be placed into the single outputs/images folder." + ) + + while True: + should_continue = prompt("\nDo you wish to continue with the import [Yn] ? ").lower() or "y" + if should_continue == "n": + print("\r\nCancelling Import") + return + elif should_continue == "y": + print() + break + + db_mapper.backup(config.TIMESTAMP_STRING) + + print() + ImportStats.time_start = datetime.datetime.utcnow() + + for filepath in import_file_list: + try: + self.import_image(filepath, board_name_option, db_mapper, config) + except sqlite3.Error as sql_ex: + print(f"A database related exception was found processing {filepath}, will continue to next file. ") + print("Exception detail:") + print(sql_ex) + ImportStats.count_file_errors += 1 + except Exception as ex: + print(f"Exception processing {filepath}, will continue to next file. ") + print("Exception detail:") + print(ex) + ImportStats.count_file_errors += 1 + + print("\r\n===============================================================================") + print(f"= Import Complete - Elpased Time: {ImportStats.get_elapsed_time_string()}") + print() + print(f"Source File(s) : {ImportStats.count_source_files}") + print(f"Total Imported : {ImportStats.count_imported}") + print(f"Skipped b/c file already exists on disk : {ImportStats.count_skipped_file_exists}") + print(f"Skipped b/c file already exists in db : {ImportStats.count_skipped_db_exists}") + print(f"Errors during import : {ImportStats.count_file_errors}") + if ImportStats.count_imported > 0: + print("\r\nBreakdown of imported files by version:") + for key, version in ImportStats.count_imported_by_version.items(): + print(f" {key:20} : {version}") + + +def main(): + try: + processor = MediaImportProcessor() + processor.process() + except KeyboardInterrupt: + print("\r\n\r\nUser cancelled execution.") + + +if __name__ == "__main__": + main() diff --git a/invokeai/frontend/web/.gitignore b/invokeai/frontend/web/.gitignore new file mode 100644 index 00000000000..d71afec17e5 --- /dev/null +++ b/invokeai/frontend/web/.gitignore @@ -0,0 +1,48 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.pnpm-store +# We want to distribute the repo +dist +dist/** +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# build stats +stats.html + +# Yarn - https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Yalc +.yalc +yalc.lock + +# vitest +tsconfig.vitest-temp.json +coverage/ +*.tgz diff --git a/invokeai/frontend/web/.prettierignore b/invokeai/frontend/web/.prettierignore new file mode 100644 index 00000000000..658baa261ed --- /dev/null +++ b/invokeai/frontend/web/.prettierignore @@ -0,0 +1,17 @@ +dist/ +public/locales/*.json +!public/locales/en.json +.husky/ +node_modules/ +patches/ +stats.html +index.html +.yarn/ +.yalc/ +*.scss +src/services/api/schema.ts +static/ +src/theme/css/overlayscrollbars.css +src/theme_/css/overlayscrollbars.css +pnpm-lock.yaml +.claude diff --git a/invokeai/frontend/web/.prettierrc.json b/invokeai/frontend/web/.prettierrc.json new file mode 100644 index 00000000000..a9576c8a4a9 --- /dev/null +++ b/invokeai/frontend/web/.prettierrc.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json.schemastore.org/prettierrc", + "trailingComma": "es5", + "printWidth": 120, + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "endOfLine": "auto", + "overrides": [ + { + "files": ["public/locales/*.json"], + "options": { + "tabWidth": 4 + } + } + ] +} diff --git a/invokeai/frontend/web/.storybook/ReduxInit.tsx b/invokeai/frontend/web/.storybook/ReduxInit.tsx new file mode 100644 index 00000000000..b4989c75564 --- /dev/null +++ b/invokeai/frontend/web/.storybook/ReduxInit.tsx @@ -0,0 +1,23 @@ +import { useGlobalModifiersInit } from '@invoke-ai/ui-library'; +import type { PropsWithChildren } from 'react'; +import { memo, useEffect } from 'react'; + +import { useAppDispatch } from '../src/app/store/storeHooks'; +import { modelChanged } from '../src/features/controlLayers/store/paramsSlice'; +/** + * Initializes some state for storybook. Must be in a different component + * so that it is run inside the redux context. + */ +export const ReduxInit = memo(({ children }: PropsWithChildren) => { + const dispatch = useAppDispatch(); + useGlobalModifiersInit(); + useEffect(() => { + dispatch( + modelChanged({ model: { key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' } }) + ); + }, [dispatch]); + + return children; +}); + +ReduxInit.displayName = 'ReduxInit'; diff --git a/invokeai/frontend/web/.storybook/main.ts b/invokeai/frontend/web/.storybook/main.ts new file mode 100644 index 00000000000..e239c7030b9 --- /dev/null +++ b/invokeai/frontend/web/.storybook/main.ts @@ -0,0 +1,16 @@ +import type { StorybookConfig } from '@storybook/react-vite'; + +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: ['@storybook/addon-links', '@storybook/addon-docs'], + + framework: { + name: '@storybook/react-vite', + options: {}, + }, + + core: { + disableTelemetry: true, + }, +}; +export default config; diff --git a/invokeai/frontend/web/.storybook/manager.ts b/invokeai/frontend/web/.storybook/manager.ts new file mode 100644 index 00000000000..b3c26112d8a --- /dev/null +++ b/invokeai/frontend/web/.storybook/manager.ts @@ -0,0 +1,6 @@ +import { addons } from 'storybook/manager-api'; +import { themes } from 'storybook/theming'; + +addons.setConfig({ + theme: themes.dark, +}); diff --git a/invokeai/frontend/web/.storybook/preview.tsx b/invokeai/frontend/web/.storybook/preview.tsx new file mode 100644 index 00000000000..eb3d0391db4 --- /dev/null +++ b/invokeai/frontend/web/.storybook/preview.tsx @@ -0,0 +1,53 @@ +import type { Preview } from '@storybook/react-vite'; +import { themes } from 'storybook/theming'; +import { $store } from 'app/store/nanostores/store'; +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import { Provider } from 'react-redux'; + +// TODO: Disabled for IDE performance issues with our translation JSON +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import translationEN from '../public/locales/en.json'; +import ThemeLocaleProvider from '../src/app/components/ThemeLocaleProvider'; +import { createStore } from '../src/app/store/store'; +import { ReduxInit } from './ReduxInit'; + +i18n.use(initReactI18next).init({ + lng: 'en', + resources: { + en: { translation: translationEN }, + }, + debug: true, + interpolation: { + escapeValue: false, + }, + returnNull: false, +}); + +const store = createStore(); +$store.set(store); + +const preview: Preview = { + decorators: [ + (Story) => { + return ( + + + + + + + + ); + }, + ], + parameters: { + docs: { + theme: themes.dark, + codePanel: true, + }, + }, +}; + +export default preview; diff --git a/invokeai/frontend/web/CLAUDE.md b/invokeai/frontend/web/CLAUDE.md new file mode 100644 index 00000000000..fc784992657 --- /dev/null +++ b/invokeai/frontend/web/CLAUDE.md @@ -0,0 +1,39 @@ +# Bash commands + +All commands should be run from `/invokeai/frontend/web/`. + +- `pnpm lint:prettier`: check formatting +- `pnpm lint:eslint`: check for linting issues +- `pnpm lint:knip`: check for unused dependencies +- `pnpm lint:dpdm`: check for dependency cycles +- `pnpm lint:tsc`: check for TypeScript issues +- `pnpm lint`: run all checks +- `pnpm fix`: automatically fix issues where possible +- `pnpm test:no-watch`: run the test suite + +# Writing Tests + +This repo uses `vitest` for unit tests. + +Tests should be colocated with the code they test, and should use the `.test.ts` suffix. + +Tests do not need to be written for code that is trivial or has no logic (e.g. simple type definitions, re-exports, etc.). We currently do not do UI tests. + +# Agents + +- Use @agent-javascript-pro and @agent-typescript-pro for JavaScript and TypeScript code generation and assistance. +- Use @frontend-developer for general frontend development tasks. + +## Workflow + +Split up tasks into smaller subtasks and handle them one at a time using an agent. Ensure each subtask is completed before moving on to the next. + +Each agent should maintain a work log in a markdown file. + +When an agent completes a task, it should: + +1. Summarize the changes made. +2. List any files that were added, modified, or deleted. +3. Commit the changes with a descriptive commit message. + +DO NOT PUSH ANY CHANGES TO THE REMOTE REPOSITORY. diff --git a/invokeai/frontend/web/README.md b/invokeai/frontend/web/README.md new file mode 100644 index 00000000000..6374ace93ff --- /dev/null +++ b/invokeai/frontend/web/README.md @@ -0,0 +1,3 @@ +# Invoke UI + + diff --git a/invokeai/frontend/web/__init__.py b/invokeai/frontend/web/__init__.py new file mode 100644 index 00000000000..e9758b27b6b --- /dev/null +++ b/invokeai/frontend/web/__init__.py @@ -0,0 +1,3 @@ +""" +Initialization file for invokeai.frontend.web +""" diff --git a/invokeai/frontend/web/eslint.config.mjs b/invokeai/frontend/web/eslint.config.mjs new file mode 100644 index 00000000000..0adc887ceb3 --- /dev/null +++ b/invokeai/frontend/web/eslint.config.mjs @@ -0,0 +1,246 @@ +import js from '@eslint/js'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import typescriptParser from '@typescript-eslint/parser'; +import pluginI18Next from 'eslint-plugin-i18next'; +import pluginImport from 'eslint-plugin-import'; +import pluginPath from 'eslint-plugin-path'; +import pluginReact from 'eslint-plugin-react'; +import pluginReactHooks from 'eslint-plugin-react-hooks'; +import pluginReactRefresh from 'eslint-plugin-react-refresh'; +import pluginSimpleImportSort from 'eslint-plugin-simple-import-sort'; +import pluginStorybook from 'eslint-plugin-storybook'; +import pluginUnusedImports from 'eslint-plugin-unused-imports'; +import globals from 'globals'; + +export default [ + js.configs.recommended, + + { + languageOptions: { + parser: typescriptParser, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + globals: { + ...globals.browser, + ...globals.node, + GlobalCompositeOperation: 'readonly', + RequestInit: 'readonly', + }, + }, + + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + + plugins: { + react: pluginReact, + '@typescript-eslint': typescriptEslint, + 'react-hooks': pluginReactHooks, + import: pluginImport, + 'unused-imports': pluginUnusedImports, + 'simple-import-sort': pluginSimpleImportSort, + 'react-refresh': pluginReactRefresh.configs.vite, + path: pluginPath, + i18next: pluginI18Next, + storybook: pluginStorybook, + }, + + rules: { + ...typescriptEslint.configs.recommended.rules, + ...pluginReact.configs.recommended.rules, + ...pluginReact.configs['jsx-runtime'].rules, + ...pluginReactHooks.configs.recommended.rules, + ...pluginStorybook.configs.recommended.rules, + + 'react/jsx-no-bind': [ + 'error', + { + allowBind: true, + }, + ], + + 'react/jsx-curly-brace-presence': [ + 'error', + { + props: 'never', + children: 'never', + }, + ], + + 'react-hooks/exhaustive-deps': 'error', + + curly: 'error', + 'no-var': 'error', + 'brace-style': 'error', + 'prefer-template': 'error', + radix: 'error', + 'space-before-blocks': 'error', + eqeqeq: 'error', + 'one-var': ['error', 'never'], + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-implied-eval': 'error', + 'no-label-var': 'error', + 'no-return-assign': 'error', + 'no-sequences': 'error', + 'no-template-curly-in-string': 'error', + 'no-throw-literal': 'error', + 'no-unmodified-loop-condition': 'error', + 'import/no-duplicates': 'error', + 'import/prefer-default-export': 'off', + 'unused-imports/no-unused-imports': 'error', + + 'unused-imports/no-unused-vars': [ + 'error', + { + vars: 'all', + varsIgnorePattern: '^_', + args: 'after-used', + argsIgnorePattern: '^_', + }, + ], + + 'simple-import-sort/imports': 'error', + 'simple-import-sort/exports': 'error', + '@typescript-eslint/no-unused-vars': 'off', + + '@typescript-eslint/ban-ts-comment': [ + 'error', + { + 'ts-expect-error': 'allow-with-description', + 'ts-ignore': true, + 'ts-nocheck': true, + 'ts-check': false, + minimumDescriptionLength: 10, + }, + ], + + '@typescript-eslint/no-empty-interface': [ + 'error', + { + allowSingleExtends: true, + }, + ], + + '@typescript-eslint/consistent-type-imports': [ + 'error', + { + prefer: 'type-imports', + fixStyle: 'separate-type-imports', + disallowTypeAnnotations: true, + }, + ], + + '@typescript-eslint/no-import-type-side-effects': 'error', + + '@typescript-eslint/consistent-type-assertions': [ + 'error', + { + assertionStyle: 'as', + }, + ], + + 'path/no-relative-imports': [ + 'error', + { + maxDepth: 0, + }, + ], + + 'no-console': 'warn', + 'no-promise-executor-return': 'error', + 'require-await': 'error', + + 'no-restricted-syntax': [ + 'error', + { + selector: 'CallExpression[callee.name="setActiveTab"]', + message: + 'setActiveTab() can only be called from use-navigation-api.tsx. Use navigationApi.switchToTab() instead.', + }, + ], + + 'no-restricted-properties': [ + 'error', + { + object: 'crypto', + property: 'randomUUID', + message: 'Use of crypto.randomUUID is not allowed as it is not available in all browsers.', + }, + { + object: 'navigator', + property: 'clipboard', + message: + 'The Clipboard API is not available by default in Firefox. Use the `useClipboard` hook instead, which wraps clipboard access to prevent errors.', + }, + ], + + // Typescript handles this for us: https://eslint.org/docs/latest/rules/no-redeclare#handled_by_typescript + 'no-redeclare': 'off', + + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: 'lodash-es', + importNames: ['isEqual'], + message: 'Please use objectEquals from @observ33r/object-equals instead.', + }, + { + name: 'lodash-es', + message: 'Please use es-toolkit instead.', + }, + { + name: 'es-toolkit', + importNames: ['isEqual'], + message: 'Please use objectEquals from @observ33r/object-equals instead.', + }, + { + name: 'zod/v3', + message: 'Import from zod instead.', + }, + ], + }, + ], + }, + + settings: { + react: { + version: 'detect', + }, + }, + }, + + { + files: ['**/use-navigation-api.tsx'], + rules: { + 'no-restricted-syntax': 'off', + }, + }, + + { + files: ['**/*.stories.tsx'], + rules: { + 'i18next/no-literal-string': 'off', + }, + }, + + { + ignores: [ + '**/dist/', + '**/static/', + '**/.husky/', + '**/node_modules/', + '**/patches/', + '**/stats.html', + '**/index.html', + '**/.yarn/', + '**/*.scss', + 'src/services/api/schema.ts', + '.prettierrc.js', + '.storybook', + ], + }, +]; diff --git a/invokeai/frontend/web/index.html b/invokeai/frontend/web/index.html new file mode 100644 index 00000000000..5ff8a29d1ca --- /dev/null +++ b/invokeai/frontend/web/index.html @@ -0,0 +1,28 @@ + + + + + + + + + + Invoke - Community Edition + + + + + +
+ + + + \ No newline at end of file diff --git a/invokeai/frontend/web/knip.ts b/invokeai/frontend/web/knip.ts new file mode 100644 index 00000000000..64dcd05485b --- /dev/null +++ b/invokeai/frontend/web/knip.ts @@ -0,0 +1,29 @@ +import type { KnipConfig } from 'knip'; + +const config: KnipConfig = { + project: ['src/**/*.{ts,tsx}!'], + ignore: [ + // This file is only used during debugging + 'src/app/store/middleware/debugLoggerMiddleware.ts', + // Autogenerated types - shouldn't ever touch these + 'src/services/api/schema.ts', + 'src/features/nodes/types/v1/**', + 'src/features/nodes/types/v2/**', + 'src/features/parameters/types/parameterSchemas.ts', + // TODO(psyche): maybe we can clean up these utils after canvas v2 release + 'src/features/controlLayers/konva/util.ts', + // Will be using this + 'src/common/hooks/useAsyncState.ts', + 'src/app/store/use-debounced-app-selector.ts', + // Auth features - exports will be used in follow-up phases + 'src/features/auth/**', + 'src/services/api/endpoints/auth.ts', + ], + ignoreBinaries: ['only-allow'], + ignoreDependencies: ['magic-string'], + paths: { + 'public/*': ['public/*'], + }, +}; + +export default config; diff --git a/invokeai/frontend/web/openapi.json b/invokeai/frontend/web/openapi.json new file mode 100644 index 00000000000..2c9526c59a9 --- /dev/null +++ b/invokeai/frontend/web/openapi.json @@ -0,0 +1,73978 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Invoke - Community Edition", + "description": "An API for invoking AI image operations", + "version": "1.0.0" + }, + "paths": { + "/api/v1/auth/status": { + "get": { + "tags": ["authentication"], + "summary": "Get Setup Status", + "description": "Check if initial administrator setup is required.\n\nReturns:\n SetupStatusResponse indicating whether setup is needed and multiuser mode status", + "operationId": "get_setup_status_api_v1_auth_status_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetupStatusResponse" + } + } + } + } + } + } + }, + "/api/v1/auth/login": { + "post": { + "tags": ["authentication"], + "summary": "Login", + "description": "Authenticate user and return access token.\n\nArgs:\n request: Login credentials (email and password)\n\nReturns:\n LoginResponse containing JWT token and user information\n\nRaises:\n HTTPException: 401 if credentials are invalid or user is inactive\n HTTPException: 403 if multiuser mode is disabled", + "operationId": "login_api_v1_auth_login_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginRequest", + "description": "Login credentials" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/auth/logout": { + "post": { + "tags": ["authentication"], + "summary": "Logout", + "description": "Logout current user.\n\nCurrently a no-op since we use stateless JWT tokens. For token invalidation in\nfuture implementations, consider:\n- Token blacklist: Store invalidated tokens in Redis/database with expiration\n- Token versioning: Add version field to user record, increment on logout\n- Short-lived tokens: Use refresh token pattern with token rotation\n- Session storage: Track active sessions server-side for revocation\n\nArgs:\n current_user: The authenticated user (validates token)\n\nReturns:\n LogoutResponse indicating success", + "operationId": "logout_api_v1_auth_logout_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LogoutResponse" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/auth/me": { + "get": { + "tags": ["authentication"], + "summary": "Get Current User Info", + "description": "Get current authenticated user's information.\n\nArgs:\n current_user: The authenticated user's token data\n\nReturns:\n UserDTO containing user information\n\nRaises:\n HTTPException: 404 if user is not found (should not happen normally)", + "operationId": "get_current_user_info_api_v1_auth_me_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDTO" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + }, + "patch": { + "tags": ["authentication"], + "summary": "Update Current User", + "description": "Update the current user's own profile.\n\nTo change the password, both ``current_password`` and ``new_password`` must\nbe provided. The current password is verified before the change is applied.\n\nArgs:\n request: Profile fields to update\n current_user: The authenticated user\n\nReturns:\n The updated user\n\nRaises:\n HTTPException: 400 if current password is incorrect or new password is weak\n HTTPException: 404 if user not found", + "operationId": "update_current_user_api_v1_auth_me_patch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserProfileUpdateRequest", + "description": "Profile fields to update" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDTO" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/auth/setup": { + "post": { + "tags": ["authentication"], + "summary": "Setup Admin", + "description": "Set up initial administrator account.\n\nThis endpoint can only be called once, when no admin user exists. It creates\nthe first admin user for the system.\n\nArgs:\n request: Admin account details (email, display_name, password)\n\nReturns:\n SetupResponse containing the created admin user\n\nRaises:\n HTTPException: 400 if admin already exists or password is weak\n HTTPException: 403 if multiuser mode is disabled", + "operationId": "setup_admin_api_v1_auth_setup_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetupRequest", + "description": "Admin account details" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetupResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/auth/generate-password": { + "get": { + "tags": ["authentication"], + "summary": "Generate Password", + "description": "Generate a strong random password.\n\nReturns a cryptographically secure random password of 16 characters\ncontaining uppercase, lowercase, digits, and punctuation.", + "operationId": "generate_password_api_v1_auth_generate_password_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneratePasswordResponse" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/auth/users": { + "get": { + "tags": ["authentication"], + "summary": "List Users", + "description": "List all users. Requires admin privileges.\n\nThe internal 'system' user (created for backward compatibility) is excluded\nfrom the results since it cannot be managed through this interface.\n\nReturns:\n List of all real users (system user excluded)", + "operationId": "list_users_api_v1_auth_users_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/UserDTO" + }, + "type": "array", + "title": "Response List Users Api V1 Auth Users Get" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + }, + "post": { + "tags": ["authentication"], + "summary": "Create User", + "description": "Create a new user. Requires admin privileges.\n\nArgs:\n request: New user details\n\nReturns:\n The created user\n\nRaises:\n HTTPException: 400 if email already exists or password is weak", + "operationId": "create_user_api_v1_auth_users_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminUserCreateRequest", + "description": "New user details" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDTO" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/auth/users/{user_id}": { + "get": { + "tags": ["authentication"], + "summary": "Get User", + "description": "Get a user by ID. Requires admin privileges.\n\nArgs:\n user_id: The user ID\n\nReturns:\n The user\n\nRaises:\n HTTPException: 404 if user not found", + "operationId": "get_user_api_v1_auth_users__user_id__get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "User ID", + "title": "User Id" + }, + "description": "User ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDTO" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["authentication"], + "summary": "Update User", + "description": "Update a user. Requires admin privileges.\n\nArgs:\n user_id: The user ID\n request: Fields to update\n\nReturns:\n The updated user\n\nRaises:\n HTTPException: 400 if password is weak\n HTTPException: 404 if user not found", + "operationId": "update_user_api_v1_auth_users__user_id__patch", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "User ID", + "title": "User Id" + }, + "description": "User ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminUserUpdateRequest", + "description": "User fields to update" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDTO" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["authentication"], + "summary": "Delete User", + "description": "Delete a user. Requires admin privileges.\n\nAdmins can delete any user including other admins, but cannot delete the last\nremaining admin.\n\nArgs:\n user_id: The user ID\n\nRaises:\n HTTPException: 400 if attempting to delete the last admin\n HTTPException: 404 if user not found", + "operationId": "delete_user_api_v1_auth_users__user_id__delete", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "User ID", + "title": "User Id" + }, + "description": "User ID" + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/utilities/dynamicprompts": { + "post": { + "tags": ["utilities"], + "summary": "Parse Dynamicprompts", + "description": "Creates a batch process", + "operationId": "parse_dynamicprompts", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_parse_dynamicprompts" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DynamicPromptsResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/utilities/expand-prompt": { + "post": { + "tags": ["utilities"], + "summary": "Expand Prompt", + "description": "Expand a brief prompt into a detailed image generation prompt using a text LLM.", + "operationId": "expand_prompt", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExpandPromptRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExpandPromptResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/utilities/image-to-prompt": { + "post": { + "tags": ["utilities"], + "summary": "Image To Prompt", + "description": "Generate a descriptive prompt from an image using a vision-language model.", + "operationId": "image_to_prompt", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageToPromptRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageToPromptResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v2/models/": { + "get": { + "tags": ["model_manager"], + "summary": "List Model Records", + "description": "Get a list of models.", + "operationId": "list_model_records", + "parameters": [ + { + "name": "base_models", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseModelType" + } + }, + { + "type": "null" + } + ], + "description": "Base models to include", + "title": "Base Models" + }, + "description": "Base models to include" + }, + { + "name": "model_type", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelType" + }, + { + "type": "null" + } + ], + "description": "The type of model to get", + "title": "Model Type" + }, + "description": "The type of model to get" + }, + { + "name": "model_name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Exact match on the name of the model", + "title": "Model Name" + }, + "description": "Exact match on the name of the model" + }, + { + "name": "model_format", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelFormat" + }, + { + "type": "null" + } + ], + "description": "Exact match on the format of the model (e.g. 'diffusers')", + "title": "Model Format" + }, + "description": "Exact match on the format of the model (e.g. 'diffusers')" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/ModelRecordOrderBy", + "description": "The field to order by", + "default": "name" + }, + "description": "The field to order by" + }, + { + "name": "direction", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/SQLiteDirection", + "description": "The direction to order by", + "default": "ASC" + }, + "description": "The direction to order by" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelsList" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/models/missing": { + "get": { + "tags": ["model_manager"], + "summary": "List Missing Models", + "description": "Get models whose files are missing from disk.\n\nThese are models that have database entries but their corresponding\nweight files have been deleted externally (not via Model Manager).", + "operationId": "list_missing_models", + "responses": { + "200": { + "description": "List of models with missing files", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelsList" + } + } + } + } + } + } + }, + "/api/v2/models/get_by_attrs": { + "get": { + "tags": ["model_manager"], + "summary": "Get Model Records By Attrs", + "description": "Gets a model by its attributes. The main use of this route is to provide backwards compatibility with the old\nmodel manager, which identified models by a combination of name, base and type.", + "operationId": "get_model_records_by_attrs", + "parameters": [ + { + "name": "name", + "in": "query", + "required": true, + "schema": { + "type": "string", + "description": "The name of the model", + "title": "Name" + }, + "description": "The name of the model" + }, + { + "name": "type", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/ModelType", + "description": "The type of the model" + }, + "description": "The type of the model" + }, + { + "name": "base", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/BaseModelType", + "description": "The base model of the model" + }, + "description": "The base model of the model" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Main_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD3_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_ZImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SDXL_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Spandrel_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config" + }, + { + "$ref": "#/components/schemas/CLIPVision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/SigLIP_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/TextLLM_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/ExternalApiModelConfig" + }, + { + "$ref": "#/components/schemas/Unknown_Config" + } + ], + "title": "Response Get Model Records By Attrs" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/models/get_by_hash": { + "get": { + "tags": ["model_manager"], + "summary": "Get Model Records By Hash", + "description": "Gets a model by its hash. This is useful for recalling models that were deleted and reinstalled,\nas the hash remains stable across reinstallations while the key (UUID) changes.", + "operationId": "get_model_records_by_hash", + "parameters": [ + { + "name": "hash", + "in": "query", + "required": true, + "schema": { + "type": "string", + "description": "The hash of the model", + "title": "Hash" + }, + "description": "The hash of the model" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Main_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD3_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_ZImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SDXL_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Spandrel_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config" + }, + { + "$ref": "#/components/schemas/CLIPVision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/SigLIP_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/TextLLM_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/ExternalApiModelConfig" + }, + { + "$ref": "#/components/schemas/Unknown_Config" + } + ], + "title": "Response Get Model Records By Hash" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/models/i/{key}": { + "get": { + "tags": ["model_manager"], + "summary": "Get Model Record", + "description": "Get a model record", + "operationId": "get_model_record", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Key of the model record to fetch.", + "title": "Key" + }, + "description": "Key of the model record to fetch." + } + ], + "responses": { + "200": { + "description": "The model configuration was retrieved successfully", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Main_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD3_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_ZImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SDXL_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Spandrel_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config" + }, + { + "$ref": "#/components/schemas/CLIPVision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/SigLIP_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/TextLLM_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/ExternalApiModelConfig" + }, + { + "$ref": "#/components/schemas/Unknown_Config" + } + ], + "title": "Response Get Model Record" + }, + "example": { + "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 + } + } + } + }, + "400": { + "description": "Bad request" + }, + "404": { + "description": "The model could not be found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["model_manager"], + "summary": "Update Model Record", + "description": "Update a model's config.", + "operationId": "update_model_record", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Unique key of model", + "title": "Key" + }, + "description": "Unique key of model" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelRecordChanges", + "description": "Model config", + "examples": [ + { + "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", + "variant": "normal" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "The model was updated successfully", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Main_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD3_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_ZImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SDXL_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Spandrel_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config" + }, + { + "$ref": "#/components/schemas/CLIPVision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/SigLIP_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/TextLLM_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/ExternalApiModelConfig" + }, + { + "$ref": "#/components/schemas/Unknown_Config" + } + ], + "title": "Response Update Model Record" + }, + "example": { + "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 + } + } + } + }, + "400": { + "description": "Bad request" + }, + "404": { + "description": "The model could not be found" + }, + "409": { + "description": "There is already a model corresponding to the new name" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["model_manager"], + "summary": "Delete Model", + "description": "Delete model record from database.\n\nThe configuration record will be removed. The corresponding weights files will be\ndeleted as well if they reside within the InvokeAI \"models\" directory.", + "operationId": "delete_model", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Unique key of model to remove from model registry.", + "title": "Key" + }, + "description": "Unique key of model to remove from model registry." + } + ], + "responses": { + "204": { + "description": "Model deleted successfully" + }, + "404": { + "description": "Model not found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/models/i/{key}/reidentify": { + "post": { + "tags": ["model_manager"], + "summary": "Reidentify Model", + "description": "Attempt to reidentify a model by re-probing its weights file.", + "operationId": "reidentify_model", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Key of the model to reidentify.", + "title": "Key" + }, + "description": "Key of the model to reidentify." + } + ], + "responses": { + "200": { + "description": "The model configuration was retrieved successfully", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Main_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD3_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_ZImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SDXL_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Spandrel_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config" + }, + { + "$ref": "#/components/schemas/CLIPVision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/SigLIP_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/TextLLM_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/ExternalApiModelConfig" + }, + { + "$ref": "#/components/schemas/Unknown_Config" + } + ], + "title": "Response Reidentify Model" + }, + "example": { + "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 + } + } + } + }, + "400": { + "description": "Bad request" + }, + "404": { + "description": "The model could not be found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/models/scan_folder": { + "get": { + "tags": ["model_manager"], + "summary": "Scan For Models", + "operationId": "scan_for_models", + "parameters": [ + { + "name": "scan_path", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "Directory path to search for models", + "title": "Scan Path" + }, + "description": "Directory path to search for models" + } + ], + "responses": { + "200": { + "description": "Directory scanned successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FoundModel" + }, + "title": "Response Scan For Models" + } + } + } + }, + "400": { + "description": "Invalid directory path" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/models/hugging_face": { + "get": { + "tags": ["model_manager"], + "summary": "Get Hugging Face Models", + "operationId": "get_hugging_face_models", + "parameters": [ + { + "name": "hugging_face_repo", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "Hugging face repo to search for models", + "title": "Hugging Face Repo" + }, + "description": "Hugging face repo to search for models" + } + ], + "responses": { + "200": { + "description": "Hugging Face repo scanned successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HuggingFaceModels" + } + } + } + }, + "400": { + "description": "Invalid hugging face repo" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/models/i/{key}/image": { + "get": { + "tags": ["model_manager"], + "summary": "Get Model Image", + "description": "Gets an image file that previews the model", + "operationId": "get_model_image", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The name of model image file to get", + "title": "Key" + }, + "description": "The name of model image file to get" + } + ], + "responses": { + "200": { + "description": "The model image was fetched successfully", + "content": { + "application/json": { + "schema": {} + } + } + }, + "400": { + "description": "Bad request" + }, + "404": { + "description": "The model image could not be found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["model_manager"], + "summary": "Update Model Image", + "operationId": "update_model_image", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Unique key of model", + "title": "Key" + }, + "description": "Unique key of model" + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_update_model_image" + } + } + } + }, + "responses": { + "200": { + "description": "The model image was updated successfully", + "content": { + "application/json": { + "schema": {} + } + } + }, + "400": { + "description": "Bad request" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["model_manager"], + "summary": "Delete Model Image", + "operationId": "delete_model_image", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Unique key of model image to remove from model_images directory.", + "title": "Key" + }, + "description": "Unique key of model image to remove from model_images directory." + } + ], + "responses": { + "204": { + "description": "Model image deleted successfully" + }, + "404": { + "description": "Model image not found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/models/i/bulk_delete": { + "post": { + "tags": ["model_manager"], + "summary": "Bulk Delete Models", + "description": "Delete multiple model records from database.\n\nThe configuration records will be removed. The corresponding weights files will be\ndeleted as well if they reside within the InvokeAI \"models\" directory.\nReturns a list of successfully deleted keys and failed deletions with error messages.", + "operationId": "bulk_delete_models", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkDeleteModelsRequest", + "description": "List of model keys to delete" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Models deleted (possibly with some failures)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkDeleteModelsResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v2/models/i/bulk_reidentify": { + "post": { + "tags": ["model_manager"], + "summary": "Bulk Reidentify Models", + "description": "Reidentify multiple models by re-probing their weights files.\n\nReturns a list of successfully reidentified keys and failed reidentifications with error messages.", + "operationId": "bulk_reidentify_models", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkReidentifyModelsRequest", + "description": "List of model keys to reidentify" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Models reidentified (possibly with some failures)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkReidentifyModelsResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v2/models/install": { + "post": { + "tags": ["model_manager"], + "summary": "Install Model", + "description": "Install a model using a string identifier.\n\n`source` can be any of the following.\n\n1. A path on the local filesystem ('C:\\users\\fred\\model.safetensors')\n2. A Url pointing to a single downloadable model file\n3. A HuggingFace repo_id with any of the following formats:\n - model/name\n - model/name:fp16:vae\n - model/name::vae -- use default precision\n - model/name:fp16:path/to/model.safetensors\n - model/name::path/to/model.safetensors\n\n`config` is a ModelRecordChanges object. Fields in this object will override\nthe ones that are probed automatically. Pass an empty object to accept\nall the defaults.\n\n`access_token` is an optional access token for use with Urls that require\nauthentication.\n\nModels will be downloaded, probed, configured and installed in a\nseries of background threads. The return object has `status` attribute\nthat can be used to monitor progress.\n\nSee the documentation for `import_model_record` for more information on\ninterpreting the job information returned by this route.", + "operationId": "install_model", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "source", + "in": "query", + "required": true, + "schema": { + "type": "string", + "description": "Model source to install, can be a local path, repo_id, or remote URL", + "title": "Source" + }, + "description": "Model source to install, can be a local path, repo_id, or remote URL" + }, + { + "name": "inplace", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Whether or not to install a local model in place", + "default": false, + "title": "Inplace" + }, + "description": "Whether or not to install a local model in place" + }, + { + "name": "access_token", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "access token for the remote resource", + "title": "Access Token" + }, + "description": "access token for the remote resource" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelRecordChanges", + "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" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "The model imported successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelInstallJob" + } + } + } + }, + "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" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": ["model_manager"], + "summary": "List Model Installs", + "description": "Return the list of model install jobs.\n\nInstall jobs have a numeric `id`, a `status`, and other fields that provide information on\nthe nature of the job and its progress. The `status` is one of:\n\n* \"waiting\" -- Job is waiting in the queue to run\n* \"downloading\" -- Model file(s) are downloading\n* \"running\" -- Model has downloaded and the model probing and registration process is running\n* \"paused\" -- Job is paused and can be resumed\n* \"completed\" -- Installation completed successfully\n* \"error\" -- An error occurred. Details will be in the \"error_type\" and \"error\" fields.\n* \"cancelled\" -- Job was cancelled before completion.\n\nOnce completed, information about the model such as its size, base\nmodel and type can be retrieved from the `config_out` field. For multi-file models such as diffusers,\ninformation on individual files can be retrieved from `download_parts`.\n\nSee the example and schema below for more information.", + "operationId": "list_model_installs", + "security": [ + { + "HTTPBearer": [] + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ModelInstallJob" + }, + "title": "Response List Model Installs" + } + } + } + } + } + }, + "delete": { + "tags": ["model_manager"], + "summary": "Prune Model Install Jobs", + "description": "Prune all completed and errored jobs from the install job list.", + "operationId": "prune_model_install_jobs", + "security": [ + { + "HTTPBearer": [] + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "204": { + "description": "All completed and errored jobs have been pruned" + }, + "400": { + "description": "Bad request" + } + } + } + }, + "/api/v2/models/install/huggingface": { + "get": { + "tags": ["model_manager"], + "summary": "Install Hugging Face Model", + "description": "Install a Hugging Face model using a string identifier.", + "operationId": "install_hugging_face_model", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "source", + "in": "query", + "required": true, + "schema": { + "type": "string", + "description": "HuggingFace repo_id to install", + "title": "Source" + }, + "description": "HuggingFace repo_id to install" + } + ], + "responses": { + "201": { + "description": "The model is being installed", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Bad request" + }, + "409": { + "description": "There is already a model corresponding to this path or repo_id" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/models/install/{id}": { + "get": { + "tags": ["model_manager"], + "summary": "Get Model Install Job", + "description": "Return model install job corresponding to the given source. See the documentation for 'List Model Install Jobs'\nfor information on the format of the return value.", + "operationId": "get_model_install_job", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "description": "Model install id", + "title": "Id" + }, + "description": "Model install id" + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelInstallJob" + } + } + } + }, + "404": { + "description": "No such job" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["model_manager"], + "summary": "Cancel Model Install Job", + "description": "Cancel the model install job(s) corresponding to the given job ID.", + "operationId": "cancel_model_install_job", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "description": "Model install job ID", + "title": "Id" + }, + "description": "Model install job ID" + } + ], + "responses": { + "201": { + "description": "The job was cancelled successfully", + "content": { + "application/json": { + "schema": {} + } + } + }, + "415": { + "description": "No such job" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/models/install/{id}/pause": { + "post": { + "tags": ["model_manager"], + "summary": "Pause Model Install Job", + "description": "Pause the model install job corresponding to the given job ID.", + "operationId": "pause_model_install_job", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "description": "Model install job ID", + "title": "Id" + }, + "description": "Model install job ID" + } + ], + "responses": { + "201": { + "description": "The job was paused successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelInstallJob" + } + } + } + }, + "415": { + "description": "No such job" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/models/install/{id}/resume": { + "post": { + "tags": ["model_manager"], + "summary": "Resume Model Install Job", + "description": "Resume a paused model install job corresponding to the given job ID.", + "operationId": "resume_model_install_job", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "description": "Model install job ID", + "title": "Id" + }, + "description": "Model install job ID" + } + ], + "responses": { + "201": { + "description": "The job was resumed successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelInstallJob" + } + } + } + }, + "415": { + "description": "No such job" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/models/install/{id}/restart_failed": { + "post": { + "tags": ["model_manager"], + "summary": "Restart Failed Model Install Job", + "description": "Restart failed or non-resumable file downloads for the given job.", + "operationId": "restart_failed_model_install_job", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "description": "Model install job ID", + "title": "Id" + }, + "description": "Model install job ID" + } + ], + "responses": { + "201": { + "description": "Failed files restarted successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelInstallJob" + } + } + } + }, + "415": { + "description": "No such job" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/models/install/{id}/restart_file": { + "post": { + "tags": ["model_manager"], + "summary": "Restart Model Install File", + "description": "Restart a specific file download for the given job.", + "operationId": "restart_model_install_file", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "description": "Model install job ID", + "title": "Id" + }, + "description": "Model install job ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "File download URL to restart", + "title": "File Source" + } + } + } + }, + "responses": { + "201": { + "description": "File restarted successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelInstallJob" + } + } + } + }, + "415": { + "description": "No such job" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/models/convert/{key}": { + "put": { + "tags": ["model_manager"], + "summary": "Convert Model", + "description": "Permanently convert a model into diffusers format, replacing the safetensors version.\nNote that during the conversion process the key and model hash will change.\nThe return value is the model configuration for the converted model.", + "operationId": "convert_model", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "Unique key of the safetensors main model to convert to diffusers format.", + "title": "Key" + }, + "description": "Unique key of the safetensors main model to convert to diffusers format." + } + ], + "responses": { + "200": { + "description": "Model converted successfully", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Main_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD3_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_ZImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SDXL_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Spandrel_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config" + }, + { + "$ref": "#/components/schemas/CLIPVision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/SigLIP_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/TextLLM_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/ExternalApiModelConfig" + }, + { + "$ref": "#/components/schemas/Unknown_Config" + } + ], + "title": "Response Convert Model" + }, + "example": { + "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 + } + } + } + }, + "400": { + "description": "Bad request" + }, + "404": { + "description": "Model not found" + }, + "409": { + "description": "There is already a model registered at this location" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/models/starter_models": { + "get": { + "tags": ["model_manager"], + "summary": "Get Starter Models", + "operationId": "get_starter_models", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StarterModelResponse" + } + } + } + } + } + } + }, + "/api/v2/models/stats": { + "get": { + "tags": ["model_manager"], + "summary": "Get model manager RAM cache performance statistics.", + "description": "Return performance statistics on the model manager's RAM cache. Will return null if no models have been loaded.", + "operationId": "get_stats", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/CacheStats" + }, + { + "type": "null" + } + ], + "title": "Response Get Stats" + } + } + } + } + } + } + }, + "/api/v2/models/empty_model_cache": { + "post": { + "tags": ["model_manager"], + "summary": "Empty Model Cache", + "description": "Drop all models from the model cache to free RAM/VRAM. 'Locked' models that are in active use will not be dropped.", + "operationId": "empty_model_cache", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v2/models/hf_login": { + "get": { + "tags": ["model_manager"], + "summary": "Get Hf Login Status", + "operationId": "get_hf_login_status", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HFTokenStatus" + } + } + } + } + } + }, + "post": { + "tags": ["model_manager"], + "summary": "Do Hf Login", + "operationId": "do_hf_login", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_do_hf_login" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HFTokenStatus" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + }, + "delete": { + "tags": ["model_manager"], + "summary": "Reset Hf Token", + "operationId": "reset_hf_token", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HFTokenStatus" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v2/models/sync/orphaned": { + "get": { + "tags": ["model_manager"], + "summary": "Get Orphaned Models", + "description": "Find orphaned model directories.\n\nOrphaned models are directories in the models folder that contain model files\nbut are not referenced in the database. This can happen when models are deleted\nfrom the database but the files remain on disk.\n\nReturns:\n List of orphaned model directory information", + "operationId": "get_orphaned_models", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/OrphanedModelInfo" + }, + "type": "array", + "title": "Response Get Orphaned Models" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + }, + "delete": { + "tags": ["model_manager"], + "summary": "Delete Orphaned Models", + "description": "Delete specified orphaned model directories.\n\nArgs:\n request: Request containing list of relative paths to delete\n\nReturns:\n Response indicating which paths were deleted and which had errors", + "operationId": "delete_orphaned_models", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteOrphanedModelsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteOrphanedModelsResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/download_queue/": { + "get": { + "tags": ["download_queue"], + "summary": "List Downloads", + "description": "Get a list of active and inactive jobs.", + "operationId": "list_downloads", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/DownloadJob" + }, + "type": "array", + "title": "Response List Downloads" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + }, + "patch": { + "tags": ["download_queue"], + "summary": "Prune Downloads", + "description": "Prune completed and errored jobs.", + "operationId": "prune_downloads", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "204": { + "description": "All completed jobs have been pruned" + }, + "400": { + "description": "Bad request" + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/download_queue/i/": { + "post": { + "tags": ["download_queue"], + "summary": "Download", + "description": "Download the source URL to the file or directory indicted in dest.", + "operationId": "download", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_download" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadJob" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/download_queue/i/{id}": { + "get": { + "tags": ["download_queue"], + "summary": "Get Download Job", + "description": "Get a download job using its ID.", + "operationId": "get_download_job", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "description": "ID of the download job to fetch.", + "title": "Id" + }, + "description": "ID of the download job to fetch." + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadJob" + } + } + } + }, + "404": { + "description": "The requested download JobID could not be found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["download_queue"], + "summary": "Cancel Download Job", + "description": "Cancel a download job using its ID.", + "operationId": "cancel_download_job", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "description": "ID of the download job to cancel.", + "title": "Id" + }, + "description": "ID of the download job to cancel." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "204": { + "description": "Job has been cancelled" + }, + "404": { + "description": "The requested download JobID could not be found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/download_queue/i": { + "delete": { + "tags": ["download_queue"], + "summary": "Cancel All Download Jobs", + "description": "Cancel all download jobs.", + "operationId": "cancel_all_download_jobs", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "204": { + "description": "Download jobs have been cancelled" + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/images/upload": { + "post": { + "tags": ["images"], + "summary": "Upload Image", + "description": "Uploads an image for the current user", + "operationId": "upload_image", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "image_category", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/ImageCategory", + "description": "The category of the image" + }, + "description": "The category of the image" + }, + { + "name": "is_intermediate", + "in": "query", + "required": true, + "schema": { + "type": "boolean", + "description": "Whether this is an intermediate image", + "title": "Is Intermediate" + }, + "description": "Whether this is an intermediate image" + }, + { + "name": "board_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The board to add this image to, if any", + "title": "Board Id" + }, + "description": "The board to add this image to, if any" + }, + { + "name": "session_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The session ID associated with this upload, if any", + "title": "Session Id" + }, + "description": "The session ID associated with this upload, if any" + }, + { + "name": "crop_visible", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Whether to crop the image", + "default": false, + "title": "Crop Visible" + }, + "description": "Whether to crop the image" + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_upload_image" + } + } + } + }, + "responses": { + "201": { + "description": "The image was uploaded successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageDTO" + } + } + } + }, + "415": { + "description": "Image upload failed" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/images/": { + "post": { + "tags": ["images"], + "summary": "Create Image Upload Entry", + "description": "Uploads an image from a URL, not implemented", + "operationId": "create_image_upload_entry", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_create_image_upload_entry" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageUploadEntry" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": ["images"], + "summary": "List Image Dtos", + "description": "Gets a list of image DTOs for the current user", + "operationId": "list_image_dtos", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "image_origin", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResourceOrigin" + }, + { + "type": "null" + } + ], + "description": "The origin of images to list.", + "title": "Image Origin" + }, + "description": "The origin of images to list." + }, + { + "name": "categories", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageCategory" + } + }, + { + "type": "null" + } + ], + "description": "The categories of image to include.", + "title": "Categories" + }, + "description": "The categories of image to include." + }, + { + "name": "is_intermediate", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Whether to list intermediate images.", + "title": "Is Intermediate" + }, + "description": "Whether to list intermediate images." + }, + { + "name": "board_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The board id to filter by. Use 'none' to find images without a board.", + "title": "Board Id" + }, + "description": "The board id to filter by. Use 'none' to find images without a board." + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "description": "The page offset", + "default": 0, + "title": "Offset" + }, + "description": "The page offset" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "description": "The number of images per page", + "default": 10, + "title": "Limit" + }, + "description": "The number of images per page" + }, + { + "name": "order_dir", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/SQLiteDirection", + "description": "The order of sort", + "default": "DESC" + }, + "description": "The order of sort" + }, + { + "name": "starred_first", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to sort by starred images first", + "default": true, + "title": "Starred First" + }, + "description": "Whether to sort by starred images first" + }, + { + "name": "search_term", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The term to search for", + "title": "Search Term" + }, + "description": "The term to search for" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OffsetPaginatedResults_ImageDTO_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/images/i/{image_name}": { + "delete": { + "tags": ["images"], + "summary": "Delete Image", + "description": "Deletes an image", + "operationId": "delete_image", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "image_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The name of the image to delete", + "title": "Image Name" + }, + "description": "The name of the image to delete" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteImagesResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["images"], + "summary": "Update Image", + "description": "Updates an image", + "operationId": "update_image", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "image_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The name of the image to update", + "title": "Image Name" + }, + "description": "The name of the image to update" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageRecordChanges", + "description": "The changes to apply to the image" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageDTO" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": ["images"], + "summary": "Get Image Dto", + "description": "Gets an image's DTO", + "operationId": "get_image_dto", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "image_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The name of image to get", + "title": "Image Name" + }, + "description": "The name of image to get" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageDTO" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/images/intermediates": { + "get": { + "tags": ["images"], + "summary": "Get Intermediates Count", + "description": "Gets the count of intermediate images. Non-admin users only see their own intermediates.", + "operationId": "get_intermediates_count", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "integer", + "title": "Response Get Intermediates Count" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + }, + "delete": { + "tags": ["images"], + "summary": "Clear Intermediates", + "description": "Clears all intermediates. Requires admin.", + "operationId": "clear_intermediates", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "integer", + "title": "Response Clear Intermediates" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/images/i/{image_name}/metadata": { + "get": { + "tags": ["images"], + "summary": "Get Image Metadata", + "description": "Gets an image's metadata", + "operationId": "get_image_metadata", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "image_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The name of image to get", + "title": "Image Name" + }, + "description": "The name of image to get" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "title": "Response Get Image Metadata" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/images/i/{image_name}/workflow": { + "get": { + "tags": ["images"], + "summary": "Get Image Workflow", + "operationId": "get_image_workflow", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "image_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The name of image whose workflow to get", + "title": "Image Name" + }, + "description": "The name of image whose workflow to get" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowAndGraphResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/images/i/{image_name}/full": { + "head": { + "tags": ["images"], + "summary": "Get Image Full", + "description": "Gets a full-resolution image file.\n\nThis endpoint is intentionally unauthenticated because browsers load images\nvia tags which cannot send Bearer tokens. Image names are UUIDs,\nproviding security through unguessability.", + "operationId": "get_image_full_head", + "parameters": [ + { + "name": "image_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The name of full-resolution image file to get", + "title": "Image Name" + }, + "description": "The name of full-resolution image file to get" + } + ], + "responses": { + "200": { + "description": "Return the full-resolution image", + "content": { + "image/png": {} + } + }, + "404": { + "description": "Image not found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": ["images"], + "summary": "Get Image Full", + "description": "Gets a full-resolution image file.\n\nThis endpoint is intentionally unauthenticated because browsers load images\nvia tags which cannot send Bearer tokens. Image names are UUIDs,\nproviding security through unguessability.", + "operationId": "get_image_full", + "parameters": [ + { + "name": "image_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The name of full-resolution image file to get", + "title": "Image Name" + }, + "description": "The name of full-resolution image file to get" + } + ], + "responses": { + "200": { + "description": "Return the full-resolution image", + "content": { + "image/png": {} + } + }, + "404": { + "description": "Image not found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/images/i/{image_name}/thumbnail": { + "get": { + "tags": ["images"], + "summary": "Get Image Thumbnail", + "description": "Gets a thumbnail image file.\n\nThis endpoint is intentionally unauthenticated because browsers load images\nvia tags which cannot send Bearer tokens. Image names are UUIDs,\nproviding security through unguessability.", + "operationId": "get_image_thumbnail", + "parameters": [ + { + "name": "image_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The name of thumbnail image file to get", + "title": "Image Name" + }, + "description": "The name of thumbnail image file to get" + } + ], + "responses": { + "200": { + "description": "Return the image thumbnail", + "content": { + "image/webp": {} + } + }, + "404": { + "description": "Image not found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/images/i/{image_name}/urls": { + "get": { + "tags": ["images"], + "summary": "Get Image Urls", + "description": "Gets an image and thumbnail URL", + "operationId": "get_image_urls", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "image_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The name of the image whose URL to get", + "title": "Image Name" + }, + "description": "The name of the image whose URL to get" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageUrlsDTO" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/images/delete": { + "post": { + "tags": ["images"], + "summary": "Delete Images From List", + "operationId": "delete_images_from_list", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_delete_images_from_list" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteImagesResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/images/uncategorized": { + "delete": { + "tags": ["images"], + "summary": "Delete Uncategorized Images", + "description": "Deletes all uncategorized images owned by the current user (or all if admin)", + "operationId": "delete_uncategorized_images", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteImagesResult" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/images/star": { + "post": { + "tags": ["images"], + "summary": "Star Images In List", + "operationId": "star_images_in_list", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_star_images_in_list" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StarredImagesResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/images/unstar": { + "post": { + "tags": ["images"], + "summary": "Unstar Images In List", + "operationId": "unstar_images_in_list", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_unstar_images_in_list" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnstarredImagesResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/images/download": { + "post": { + "tags": ["images"], + "summary": "Download Images From List", + "operationId": "download_images_from_list", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_download_images_from_list" + } + } + } + }, + "responses": { + "202": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImagesDownloaded" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/images/download/{bulk_download_item_name}": { + "get": { + "tags": ["images"], + "summary": "Get Bulk Download Item", + "description": "Gets a bulk download zip file.\n\nRequires authentication. The caller must be the user who initiated the\ndownload (tracked by the bulk download service) or an admin.", + "operationId": "get_bulk_download_item", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "bulk_download_item_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The bulk_download_item_name of the bulk download item to get", + "title": "Bulk Download Item Name" + }, + "description": "The bulk_download_item_name of the bulk download item to get" + } + ], + "responses": { + "200": { + "description": "Return the complete bulk download item", + "content": { + "application/zip": {} + } + }, + "404": { + "description": "Image not found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/images/names": { + "get": { + "tags": ["images"], + "summary": "Get Image Names", + "description": "Gets ordered list of image names with metadata for optimistic updates", + "operationId": "get_image_names", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "image_origin", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResourceOrigin" + }, + { + "type": "null" + } + ], + "description": "The origin of images to list.", + "title": "Image Origin" + }, + "description": "The origin of images to list." + }, + { + "name": "categories", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageCategory" + } + }, + { + "type": "null" + } + ], + "description": "The categories of image to include.", + "title": "Categories" + }, + "description": "The categories of image to include." + }, + { + "name": "is_intermediate", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Whether to list intermediate images.", + "title": "Is Intermediate" + }, + "description": "Whether to list intermediate images." + }, + { + "name": "board_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The board id to filter by. Use 'none' to find images without a board.", + "title": "Board Id" + }, + "description": "The board id to filter by. Use 'none' to find images without a board." + }, + { + "name": "order_dir", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/SQLiteDirection", + "description": "The order of sort", + "default": "DESC" + }, + "description": "The order of sort" + }, + { + "name": "starred_first", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to sort by starred images first", + "default": true, + "title": "Starred First" + }, + "description": "Whether to sort by starred images first" + }, + { + "name": "search_term", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The term to search for", + "title": "Search Term" + }, + "description": "The term to search for" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageNamesResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/images/images_by_names": { + "post": { + "tags": ["images"], + "summary": "Get Images By Names", + "description": "Gets image DTOs for the specified image names. Maintains order of input names.", + "operationId": "get_images_by_names", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_get_images_by_names" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ImageDTO" + }, + "type": "array", + "title": "Response 200 Get Images By Names" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/boards/": { + "post": { + "tags": ["boards"], + "summary": "Create Board", + "description": "Creates a board for the current user", + "operationId": "create_board", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "board_name", + "in": "query", + "required": true, + "schema": { + "type": "string", + "maxLength": 300, + "description": "The name of the board to create", + "title": "Board Name" + }, + "description": "The name of the board to create" + } + ], + "responses": { + "201": { + "description": "The board was created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BoardDTO" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": ["boards"], + "summary": "List Boards", + "description": "Gets a list of boards for the current user, including shared boards. Admin users see all boards.", + "operationId": "list_boards", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/BoardRecordOrderBy", + "description": "The attribute to order by", + "default": "created_at" + }, + "description": "The attribute to order by" + }, + { + "name": "direction", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/SQLiteDirection", + "description": "The direction to order by", + "default": "DESC" + }, + "description": "The direction to order by" + }, + { + "name": "all", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Whether to list all boards", + "title": "All" + }, + "description": "Whether to list all boards" + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "The page offset", + "title": "Offset" + }, + "description": "The page offset" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "The number of boards per page", + "title": "Limit" + }, + "description": "The number of boards per page" + }, + { + "name": "include_archived", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether or not to include archived boards in list", + "default": false, + "title": "Include Archived" + }, + "description": "Whether or not to include archived boards in list" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/OffsetPaginatedResults_BoardDTO_" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/BoardDTO" + } + } + ], + "title": "Response List Boards" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/boards/{board_id}": { + "get": { + "tags": ["boards"], + "summary": "Get Board", + "description": "Gets a board (user must have access to it)", + "operationId": "get_board", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "board_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The id of board to get", + "title": "Board Id" + }, + "description": "The id of board to get" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BoardDTO" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["boards"], + "summary": "Update Board", + "description": "Updates a board (user must have access to it)", + "operationId": "update_board", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "board_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The id of board to update", + "title": "Board Id" + }, + "description": "The id of board to update" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BoardChanges", + "description": "The changes to apply to the board" + } + } + } + }, + "responses": { + "201": { + "description": "The board was updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BoardDTO" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["boards"], + "summary": "Delete Board", + "description": "Deletes a board (user must have access to it)", + "operationId": "delete_board", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "board_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The id of board to delete", + "title": "Board Id" + }, + "description": "The id of board to delete" + }, + { + "name": "include_images", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Permanently delete all images on the board", + "default": false, + "title": "Include Images" + }, + "description": "Permanently delete all images on the board" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteBoardResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/boards/{board_id}/image_names": { + "get": { + "tags": ["boards"], + "summary": "List All Board Image Names", + "description": "Gets a list of images for a board", + "operationId": "list_all_board_image_names", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "board_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The id of the board or 'none' for uncategorized images", + "title": "Board Id" + }, + "description": "The id of the board or 'none' for uncategorized images" + }, + { + "name": "categories", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageCategory" + } + }, + { + "type": "null" + } + ], + "description": "The categories of image to include.", + "title": "Categories" + }, + "description": "The categories of image to include." + }, + { + "name": "is_intermediate", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Whether to list intermediate images.", + "title": "Is Intermediate" + }, + "description": "Whether to list intermediate images." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Response List All Board Image Names" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/board_images/": { + "post": { + "tags": ["boards"], + "summary": "Add Image To Board", + "description": "Creates a board_image", + "operationId": "add_image_to_board", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_add_image_to_board" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "The image was added to a board successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddImagesToBoardResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + }, + "delete": { + "tags": ["boards"], + "summary": "Remove Image From Board", + "description": "Removes an image from its board, if it had one", + "operationId": "remove_image_from_board", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_remove_image_from_board" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "The image was removed from the board successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoveImagesFromBoardResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/board_images/batch": { + "post": { + "tags": ["boards"], + "summary": "Add Images To Board", + "description": "Adds a list of images to a board", + "operationId": "add_images_to_board", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_add_images_to_board" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Images were added to board successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddImagesToBoardResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/board_images/batch/delete": { + "post": { + "tags": ["boards"], + "summary": "Remove Images From Board", + "description": "Removes a list of images from their board, if they had one", + "operationId": "remove_images_from_board", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_remove_images_from_board" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Images were removed from board successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoveImagesFromBoardResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/virtual_boards/by_date": { + "get": { + "tags": ["virtual_boards"], + "summary": "List Virtual Boards By Date", + "description": "Gets a list of virtual sub-boards grouped by date.", + "operationId": "list_virtual_boards_by_date", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/VirtualSubBoardDTO" + }, + "type": "array", + "title": "Response List Virtual Boards By Date" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/virtual_boards/by_date/{date}/image_names": { + "get": { + "tags": ["virtual_boards"], + "summary": "List Virtual Board Image Names By Date", + "description": "Gets ordered image names for a specific date.", + "operationId": "list_virtual_board_image_names_by_date", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "date", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The ISO date string, e.g. '2026-03-18'", + "title": "Date" + }, + "description": "The ISO date string, e.g. '2026-03-18'" + }, + { + "name": "starred_first", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to sort starred images first", + "default": true, + "title": "Starred First" + }, + "description": "Whether to sort starred images first" + }, + { + "name": "order_dir", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/SQLiteDirection", + "description": "The sort direction", + "default": "DESC" + }, + "description": "The sort direction" + }, + { + "name": "categories", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImageCategory" + } + }, + { + "type": "null" + } + ], + "description": "The categories of images to include", + "title": "Categories" + }, + "description": "The categories of images to include" + }, + { + "name": "search_term", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search term to filter images", + "title": "Search Term" + }, + "description": "Search term to filter images" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageNamesResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/model_relationships/i/{model_key}": { + "get": { + "tags": ["model_relationships"], + "summary": "Get Related Models", + "description": "Get a list of model keys related to a given model.", + "operationId": "get_related_models", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "model_key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The key of the model to get relationships for", + "title": "Model Key" + }, + "description": "The key of the model to get relationships for" + } + ], + "responses": { + "200": { + "description": "A list of related model keys was retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Response Get Related Models" + }, + "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" + } + } + } + }, + "/api/v1/model_relationships/": { + "post": { + "tags": ["model_relationships"], + "summary": "Add Model Relationship", + "description": "Creates a **bidirectional** relationship between two models, allowing each to reference the other as related.", + "operationId": "add_model_relationship_api_v1_model_relationships__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelRelationshipCreateRequest", + "description": "The model keys to relate" + } + } + }, + "required": true + }, + "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" + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + }, + "delete": { + "tags": ["model_relationships"], + "summary": "Remove Model Relationship", + "description": "Removes a **bidirectional** relationship between two models. The relationship must already exist.", + "operationId": "remove_model_relationship_api_v1_model_relationships__delete", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelRelationshipCreateRequest", + "description": "The model keys to disconnect" + } + } + }, + "required": true + }, + "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" + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/model_relationships/batch": { + "post": { + "tags": ["model_relationships"], + "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.", + "operationId": "get_related_models_batch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelRelationshipBatchRequest", + "description": "Model keys to check for related connections" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Related model keys retrieved successfully", + "content": { + "application/json": { + "schema": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Response Get Related Models Batch" + }, + "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" + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/app/version": { + "get": { + "tags": ["app"], + "summary": "Get Version", + "operationId": "app_version", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AppVersion" + } + } + } + } + } + } + }, + "/api/v1/app/app_deps": { + "get": { + "tags": ["app"], + "summary": "Get App Deps", + "operationId": "get_app_deps", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Response Get App Deps" + } + } + } + } + } + } + }, + "/api/v1/app/patchmatch_status": { + "get": { + "tags": ["app"], + "summary": "Get Patchmatch Status", + "operationId": "get_patchmatch_status", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "boolean", + "title": "Response Get Patchmatch Status" + } + } + } + } + } + } + }, + "/api/v1/app/runtime_config": { + "get": { + "tags": ["app"], + "summary": "Get Runtime Config", + "operationId": "get_runtime_config", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvokeAIAppConfigWithSetFields" + } + } + } + } + } + }, + "patch": { + "tags": ["app"], + "summary": "Update Runtime Config", + "operationId": "update_runtime_config", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAppGenerationSettingsRequest", + "description": "Writable runtime configuration changes" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvokeAIAppConfigWithSetFields" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/app/external_providers/status": { + "get": { + "tags": ["app"], + "summary": "Get External Provider Statuses", + "operationId": "get_external_provider_statuses", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ExternalProviderStatusModel" + }, + "type": "array", + "title": "Response Get External Provider Statuses" + } + } + } + } + } + } + }, + "/api/v1/app/external_providers/config": { + "get": { + "tags": ["app"], + "summary": "Get External Provider Configs", + "operationId": "get_external_provider_configs", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ExternalProviderConfigModel" + }, + "type": "array", + "title": "Response Get External Provider Configs" + } + } + } + } + } + } + }, + "/api/v1/app/external_providers/config/{provider_id}": { + "post": { + "tags": ["app"], + "summary": "Set External Provider Config", + "operationId": "set_external_provider_config", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "provider_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The external provider identifier", + "title": "Provider Id" + }, + "description": "The external provider identifier" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalProviderConfigUpdate", + "description": "External provider configuration settings" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalProviderConfigModel" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["app"], + "summary": "Reset External Provider Config", + "operationId": "reset_external_provider_config", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "provider_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The external provider identifier", + "title": "Provider Id" + }, + "description": "The external provider identifier" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalProviderConfigModel" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/app/logging": { + "get": { + "tags": ["app"], + "summary": "Get Log Level", + "description": "Returns the log level", + "operationId": "get_log_level", + "responses": { + "200": { + "description": "The operation was successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LogLevel" + } + } + } + } + } + }, + "post": { + "tags": ["app"], + "summary": "Set Log Level", + "description": "Sets the log verbosity level", + "operationId": "set_log_level", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LogLevel", + "description": "New log verbosity level" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "The operation was successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LogLevel" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/app/invocation_cache": { + "delete": { + "tags": ["app"], + "summary": "Clear Invocation Cache", + "description": "Clears the invocation cache", + "operationId": "clear_invocation_cache", + "responses": { + "200": { + "description": "The operation was successful", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/api/v1/app/invocation_cache/enable": { + "put": { + "tags": ["app"], + "summary": "Enable Invocation Cache", + "description": "Clears the invocation cache", + "operationId": "enable_invocation_cache", + "responses": { + "200": { + "description": "The operation was successful", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/api/v1/app/invocation_cache/disable": { + "put": { + "tags": ["app"], + "summary": "Disable Invocation Cache", + "description": "Clears the invocation cache", + "operationId": "disable_invocation_cache", + "responses": { + "200": { + "description": "The operation was successful", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/api/v1/app/invocation_cache/status": { + "get": { + "tags": ["app"], + "summary": "Get Invocation Cache Status", + "description": "Clears the invocation cache", + "operationId": "get_invocation_cache_status", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvocationCacheStatus" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/enqueue_batch": { + "post": { + "tags": ["queue"], + "summary": "Enqueue Batch", + "description": "Processes a batch and enqueues the output graphs for execution for the current user.", + "operationId": "enqueue_batch", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_enqueue_batch" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnqueueBatchResult" + } + } + } + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnqueueBatchResult" + } + } + }, + "description": "Created" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/list_all": { + "get": { + "tags": ["queue"], + "summary": "List All Queue Items", + "description": "Gets all queue items", + "operationId": "list_all_queue_items", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + }, + { + "name": "destination", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The destination of queue items to fetch", + "title": "Destination" + }, + "description": "The destination of queue items to fetch" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionQueueItem" + }, + "title": "Response 200 List All Queue Items" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/item_ids": { + "get": { + "tags": ["queue"], + "summary": "Get Queue Item Ids", + "description": "Gets all queue item ids that match the given parameters. Non-admin users only see their own items.", + "operationId": "get_queue_item_ids", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + }, + { + "name": "order_dir", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/SQLiteDirection", + "description": "The order of sort", + "default": "DESC" + }, + "description": "The order of sort" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemIdsResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/items_by_ids": { + "post": { + "tags": ["queue"], + "summary": "Get Queue Items By Item Ids", + "description": "Gets queue items for the specified queue item ids. Maintains order of item ids.", + "operationId": "get_queue_items_by_item_ids", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_get_queue_items_by_item_ids" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionQueueItem" + }, + "title": "Response 200 Get Queue Items By Item Ids" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/processor/resume": { + "put": { + "tags": ["queue"], + "summary": "Resume", + "description": "Resumes session processor. Admin only.", + "operationId": "resume", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionProcessorStatus" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/processor/pause": { + "put": { + "tags": ["queue"], + "summary": "Pause", + "description": "Pauses session processor. Admin only.", + "operationId": "pause", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionProcessorStatus" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/cancel_all_except_current": { + "put": { + "tags": ["queue"], + "summary": "Cancel All Except Current", + "description": "Immediately cancels all queue items except in-processing items. Non-admin users can only cancel their own items.", + "operationId": "cancel_all_except_current", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelAllExceptCurrentResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/delete_all_except_current": { + "put": { + "tags": ["queue"], + "summary": "Delete All Except Current", + "description": "Immediately deletes all queue items except in-processing items. Non-admin users can only delete their own items.", + "operationId": "delete_all_except_current", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteAllExceptCurrentResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/cancel_by_batch_ids": { + "put": { + "tags": ["queue"], + "summary": "Cancel By Batch Ids", + "description": "Immediately cancels all queue items from the given batch ids. Non-admin users can only cancel their own items.", + "operationId": "cancel_by_batch_ids", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_cancel_by_batch_ids" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelByBatchIDsResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/cancel_by_destination": { + "put": { + "tags": ["queue"], + "summary": "Cancel By Destination", + "description": "Immediately cancels all queue items with the given destination. Non-admin users can only cancel their own items.", + "operationId": "cancel_by_destination", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + }, + { + "name": "destination", + "in": "query", + "required": true, + "schema": { + "type": "string", + "description": "The destination to cancel all queue items for", + "title": "Destination" + }, + "description": "The destination to cancel all queue items for" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelByDestinationResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/retry_items_by_id": { + "put": { + "tags": ["queue"], + "summary": "Retry Items By Id", + "description": "Retries the given queue items. Users can only retry their own items unless they are an admin.", + "operationId": "retry_items_by_id", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "The queue item ids to retry", + "title": "Item Ids" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RetryItemsResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/clear": { + "put": { + "tags": ["queue"], + "summary": "Clear", + "description": "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.", + "operationId": "clear", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClearResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/prune": { + "put": { + "tags": ["queue"], + "summary": "Prune", + "description": "Prunes all completed or errored queue items. Non-admin users can only prune their own items.", + "operationId": "prune", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PruneResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/current": { + "get": { + "tags": ["queue"], + "summary": "Get Current Queue Item", + "description": "Gets the currently execution queue item", + "operationId": "get_current_queue_item", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionQueueItem" + }, + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SessionQueueItem" + }, + { + "type": "null" + } + ], + "title": "Response 200 Get Current Queue Item" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/next": { + "get": { + "tags": ["queue"], + "summary": "Get Next Queue Item", + "description": "Gets the next queue item, without executing it", + "operationId": "get_next_queue_item", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionQueueItem" + }, + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SessionQueueItem" + }, + { + "type": "null" + } + ], + "title": "Response 200 Get Next Queue Item" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/status": { + "get": { + "tags": ["queue"], + "summary": "Get Queue Status", + "description": "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.", + "operationId": "get_queue_status", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionQueueAndProcessorStatus" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/b/{batch_id}/status": { + "get": { + "tags": ["queue"], + "summary": "Get Batch Status", + "description": "Gets the status of a batch. Non-admin users only see their own batches.", + "operationId": "get_batch_status", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + }, + { + "name": "batch_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The batch to get the status of", + "title": "Batch Id" + }, + "description": "The batch to get the status of" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchStatus" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/i/{item_id}": { + "get": { + "tags": ["queue"], + "summary": "Get Queue Item", + "description": "Gets a queue item", + "operationId": "get_queue_item", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + }, + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "description": "The queue item to get", + "title": "Item Id" + }, + "description": "The queue item to get" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionQueueItem" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["queue"], + "summary": "Delete Queue Item", + "description": "Deletes a queue item. Users can only delete their own items unless they are an admin.", + "operationId": "delete_queue_item", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + }, + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "description": "The queue item to delete", + "title": "Item Id" + }, + "description": "The queue item to delete" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/i/{item_id}/cancel": { + "put": { + "tags": ["queue"], + "summary": "Cancel Queue Item", + "description": "Cancels a queue item. Users can only cancel their own items unless they are an admin.", + "operationId": "cancel_queue_item", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + }, + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "description": "The queue item to cancel", + "title": "Item Id" + }, + "description": "The queue item to cancel" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionQueueItem" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/counts_by_destination": { + "get": { + "tags": ["queue"], + "summary": "Counts By Destination", + "description": "Gets the counts of queue items by destination. Non-admin users only see their own items.", + "operationId": "counts_by_destination", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to query", + "title": "Queue Id" + }, + "description": "The queue id to query" + }, + { + "name": "destination", + "in": "query", + "required": true, + "schema": { + "type": "string", + "description": "The destination to query", + "title": "Destination" + }, + "description": "The destination to query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionQueueCountsByDestination" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/queue/{queue_id}/d/{destination}": { + "delete": { + "tags": ["queue"], + "summary": "Delete By Destination", + "description": "Deletes all items with the given destination. Non-admin users can only delete their own items.", + "operationId": "delete_by_destination", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to query", + "title": "Queue Id" + }, + "description": "The queue id to query" + }, + { + "name": "destination", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The destination to query", + "title": "Destination" + }, + "description": "The destination to query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteByDestinationResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/workflows/i/{workflow_id}": { + "get": { + "tags": ["workflows"], + "summary": "Get Workflow", + "description": "Gets a workflow", + "operationId": "get_workflow", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "workflow_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The workflow to get", + "title": "Workflow Id" + }, + "description": "The workflow to get" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowRecordWithThumbnailDTO" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["workflows"], + "summary": "Update Workflow", + "description": "Updates a workflow", + "operationId": "update_workflow", + "security": [ + { + "HTTPBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_workflow" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowRecordDTO" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["workflows"], + "summary": "Delete Workflow", + "description": "Deletes a workflow", + "operationId": "delete_workflow", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "workflow_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The workflow to delete", + "title": "Workflow Id" + }, + "description": "The workflow to delete" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/workflows/": { + "post": { + "tags": ["workflows"], + "summary": "Create Workflow", + "description": "Creates a workflow", + "operationId": "create_workflow", + "security": [ + { + "HTTPBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_create_workflow" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowRecordDTO" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": ["workflows"], + "summary": "List Workflows", + "description": "Gets a page of workflows", + "operationId": "list_workflows", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "description": "The page to get", + "default": 0, + "title": "Page" + }, + "description": "The page to get" + }, + { + "name": "per_page", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "The number of workflows per page", + "title": "Per Page" + }, + "description": "The number of workflows per page" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/WorkflowRecordOrderBy", + "description": "The attribute to order by", + "default": "name" + }, + "description": "The attribute to order by" + }, + { + "name": "direction", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/SQLiteDirection", + "description": "The direction to order by", + "default": "ASC" + }, + "description": "The direction to order by" + }, + { + "name": "categories", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkflowCategory" + } + }, + { + "type": "null" + } + ], + "description": "The categories of workflow to get", + "title": "Categories" + }, + "description": "The categories of workflow to get" + }, + { + "name": "tags", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "The tags of workflow to get", + "title": "Tags" + }, + "description": "The tags of workflow to get" + }, + { + "name": "query", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The text to query by (matches name and description)", + "title": "Query" + }, + "description": "The text to query by (matches name and description)" + }, + { + "name": "has_been_opened", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Whether to include/exclude recent workflows", + "title": "Has Been Opened" + }, + "description": "Whether to include/exclude recent workflows" + }, + { + "name": "is_public", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Filter by public/shared status", + "title": "Is Public" + }, + "description": "Filter by public/shared status" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedResults_WorkflowRecordListItemWithThumbnailDTO_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/workflows/i/{workflow_id}/thumbnail": { + "put": { + "tags": ["workflows"], + "summary": "Set Workflow Thumbnail", + "description": "Sets a workflow's thumbnail image", + "operationId": "set_workflow_thumbnail", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "workflow_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The workflow to update", + "title": "Workflow Id" + }, + "description": "The workflow to update" + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_set_workflow_thumbnail" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowRecordDTO" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["workflows"], + "summary": "Delete Workflow Thumbnail", + "description": "Removes a workflow's thumbnail image", + "operationId": "delete_workflow_thumbnail", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "workflow_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The workflow to update", + "title": "Workflow Id" + }, + "description": "The workflow to update" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowRecordDTO" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": ["workflows"], + "summary": "Get Workflow Thumbnail", + "description": "Gets a workflow's thumbnail image.\n\nThis endpoint is intentionally unauthenticated because browsers load images\nvia tags which cannot send Bearer tokens. Workflow IDs are UUIDs,\nproviding security through unguessability.", + "operationId": "get_workflow_thumbnail", + "parameters": [ + { + "name": "workflow_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The id of the workflow thumbnail to get", + "title": "Workflow Id" + }, + "description": "The id of the workflow thumbnail to get" + } + ], + "responses": { + "200": { + "description": "The workflow thumbnail was fetched successfully", + "content": { + "application/json": { + "schema": {} + } + } + }, + "400": { + "description": "Bad request" + }, + "404": { + "description": "The workflow thumbnail could not be found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/workflows/i/{workflow_id}/is_public": { + "patch": { + "tags": ["workflows"], + "summary": "Update Workflow Is Public", + "description": "Updates whether a workflow is shared publicly", + "operationId": "update_workflow_is_public", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "workflow_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The workflow to update", + "title": "Workflow Id" + }, + "description": "The workflow to update" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_workflow_is_public" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowRecordDTO" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/workflows/tags": { + "get": { + "tags": ["workflows"], + "summary": "Get All Tags", + "description": "Gets all unique tags from workflows", + "operationId": "get_all_tags", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "categories", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkflowCategory" + } + }, + { + "type": "null" + } + ], + "description": "The categories to include", + "title": "Categories" + }, + "description": "The categories to include" + }, + { + "name": "is_public", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Filter by public/shared status", + "title": "Is Public" + }, + "description": "Filter by public/shared status" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Response Get All Tags" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/workflows/counts_by_tag": { + "get": { + "tags": ["workflows"], + "summary": "Get Counts By Tag", + "description": "Counts workflows by tag", + "operationId": "get_counts_by_tag", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "tags", + "in": "query", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The tags to get counts for", + "title": "Tags" + }, + "description": "The tags to get counts for" + }, + { + "name": "categories", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkflowCategory" + } + }, + { + "type": "null" + } + ], + "description": "The categories to include", + "title": "Categories" + }, + "description": "The categories to include" + }, + { + "name": "has_been_opened", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Whether to include/exclude recent workflows", + "title": "Has Been Opened" + }, + "description": "Whether to include/exclude recent workflows" + }, + { + "name": "is_public", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Filter by public/shared status", + "title": "Is Public" + }, + "description": "Filter by public/shared status" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer" + }, + "title": "Response Get Counts By Tag" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/workflows/counts_by_category": { + "get": { + "tags": ["workflows"], + "summary": "Counts By Category", + "description": "Counts workflows by category", + "operationId": "counts_by_category", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "categories", + "in": "query", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkflowCategory" + }, + "description": "The categories to include", + "title": "Categories" + }, + "description": "The categories to include" + }, + { + "name": "has_been_opened", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Whether to include/exclude recent workflows", + "title": "Has Been Opened" + }, + "description": "Whether to include/exclude recent workflows" + }, + { + "name": "is_public", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Filter by public/shared status", + "title": "Is Public" + }, + "description": "Filter by public/shared status" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer" + }, + "title": "Response Counts By Category" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/workflows/i/{workflow_id}/opened_at": { + "put": { + "tags": ["workflows"], + "summary": "Update Opened At", + "description": "Updates the opened_at field of a workflow", + "operationId": "update_opened_at", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "workflow_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The workflow to update", + "title": "Workflow Id" + }, + "description": "The workflow to update" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/style_presets/i/{style_preset_id}": { + "get": { + "tags": ["style_presets"], + "summary": "Get Style Preset", + "description": "Gets a style preset", + "operationId": "get_style_preset", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "style_preset_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The style preset to get", + "title": "Style Preset Id" + }, + "description": "The style preset to get" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StylePresetRecordWithImage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["style_presets"], + "summary": "Update Style Preset", + "description": "Updates a style preset", + "operationId": "update_style_preset", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "style_preset_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The id of the style preset to update", + "title": "Style Preset Id" + }, + "description": "The id of the style preset to update" + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_update_style_preset" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StylePresetRecordWithImage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["style_presets"], + "summary": "Delete Style Preset", + "description": "Deletes a style preset", + "operationId": "delete_style_preset", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "style_preset_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The style preset to delete", + "title": "Style Preset Id" + }, + "description": "The style preset to delete" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/style_presets/": { + "get": { + "tags": ["style_presets"], + "summary": "List Style Presets", + "description": "Gets the style presets visible to the current user.", + "operationId": "list_style_presets", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/StylePresetRecordWithImage" + }, + "type": "array", + "title": "Response 200 List Style Presets" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + }, + "post": { + "tags": ["style_presets"], + "summary": "Create Style Preset", + "description": "Creates a style preset", + "operationId": "create_style_preset", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_style_preset" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StylePresetRecordWithImage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/style_presets/i/{style_preset_id}/image": { + "get": { + "tags": ["style_presets"], + "summary": "Get Style Preset Image", + "description": "Gets an image file that previews the model", + "operationId": "get_style_preset_image", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "style_preset_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The id of the style preset image to get", + "title": "Style Preset Id" + }, + "description": "The id of the style preset image to get" + } + ], + "responses": { + "200": { + "description": "The style preset image was fetched successfully", + "content": { + "application/json": { + "schema": {} + } + } + }, + "400": { + "description": "Bad request" + }, + "404": { + "description": "The style preset image could not be found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/style_presets/export": { + "get": { + "tags": ["style_presets"], + "summary": "Export Style Presets", + "operationId": "export_style_presets", + "responses": { + "200": { + "description": "A CSV file with the requested data.", + "content": { + "application/json": { + "schema": {} + }, + "text/csv": {} + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/style_presets/import": { + "post": { + "tags": ["style_presets"], + "summary": "Import Style Presets", + "operationId": "import_style_presets", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_import_style_presets" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v1/client_state/{queue_id}/get_by_key": { + "get": { + "tags": ["client_state"], + "summary": "Get Client State By Key", + "description": "Gets the client state for the current user (or system user if not authenticated)", + "operationId": "get_client_state_by_key", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id (ignored, kept for backwards compatibility)", + "title": "Queue Id" + }, + "description": "The queue id (ignored, kept for backwards compatibility)" + }, + { + "name": "key", + "in": "query", + "required": true, + "schema": { + "type": "string", + "description": "Key to get", + "title": "Key" + }, + "description": "Key to get" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Response Get Client State By Key" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/client_state/{queue_id}/set_by_key": { + "post": { + "tags": ["client_state"], + "summary": "Set Client State", + "description": "Sets the client state for the current user (or system user if not authenticated)", + "operationId": "set_client_state", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id (ignored, kept for backwards compatibility)", + "title": "Queue Id" + }, + "description": "The queue id (ignored, kept for backwards compatibility)" + }, + { + "name": "key", + "in": "query", + "required": true, + "schema": { + "type": "string", + "description": "Key to set", + "title": "Key" + }, + "description": "Key to set" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string", + "description": "Stringified value to set", + "title": "Value" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "string", + "title": "Response Set Client State" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/client_state/{queue_id}/get_keys_by_prefix": { + "get": { + "tags": ["client_state"], + "summary": "Get Client State Keys By Prefix", + "description": "Gets client state keys matching a prefix for the current user", + "operationId": "get_client_state_keys_by_prefix", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id (ignored, kept for backwards compatibility)", + "title": "Queue Id" + }, + "description": "The queue id (ignored, kept for backwards compatibility)" + }, + { + "name": "prefix", + "in": "query", + "required": true, + "schema": { + "type": "string", + "description": "Prefix to filter keys by", + "title": "Prefix" + }, + "description": "Prefix to filter keys by" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Response Get Client State Keys By Prefix" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/client_state/{queue_id}/delete_by_key": { + "post": { + "tags": ["client_state"], + "summary": "Delete Client State By Key", + "description": "Deletes a specific client state key for the current user", + "operationId": "delete_client_state_by_key", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id (ignored, kept for backwards compatibility)", + "title": "Queue Id" + }, + "description": "The queue id (ignored, kept for backwards compatibility)" + }, + { + "name": "key", + "in": "query", + "required": true, + "schema": { + "type": "string", + "description": "Key to delete", + "title": "Key" + }, + "description": "Key to delete" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "204": { + "description": "Client state key deleted" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/client_state/{queue_id}/delete": { + "post": { + "tags": ["client_state"], + "summary": "Delete Client State", + "description": "Deletes the client state for the current user (or system user if not authenticated)", + "operationId": "delete_client_state", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id (ignored, kept for backwards compatibility)", + "title": "Queue Id" + }, + "description": "The queue id (ignored, kept for backwards compatibility)" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "204": { + "description": "Client state deleted" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/recall/{queue_id}": { + "post": { + "tags": ["recall"], + "summary": "Update Recall Parameters", + "description": "Update recallable parameters that can be recalled on the frontend.\n\nThis endpoint allows updating parameters such as prompt, model, steps, and other\ngeneration settings. These parameters are stored in client state and can be\naccessed by the frontend to populate UI elements.\n\nArgs:\n queue_id: The queue ID to associate these parameters with\n parameters: The RecallParameter object containing the parameters to update\n strict: When true, parameters not included in the request body are reset\n to their defaults (cleared on the frontend). Defaults to false,\n which preserves the existing behaviour of only updating the\n parameters that are explicitly provided.\n\nReturns:\n A dictionary containing the updated parameters and status\n\nExample:\n POST /api/v1/recall/{queue_id}?strict=true\n {\n \"positive_prompt\": \"a beautiful landscape\",\n \"model\": \"sd-1.5\",\n \"steps\": 20\n }\n # In strict mode, all other parameters (reference_images, loras, etc.)\n # are cleared. In non-strict mode (default) they would be left as-is.", + "operationId": "update_recall_parameters", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to perform this operation on", + "title": "Queue Id" + }, + "description": "The queue id to perform this operation on" + }, + { + "name": "strict", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "When true, parameters not included in the request are reset to their defaults (cleared).", + "default": false, + "title": "Strict" + }, + "description": "When true, parameters not included in the request are reset to their defaults (cleared)." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecallParameter", + "description": "Recall parameters to update" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Update Recall Parameters" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": ["recall"], + "summary": "Get Recall Parameters", + "description": "Retrieve all stored recall parameters for a given queue.\n\nReturns a dictionary of all recall parameters that have been set for the queue.\n\nArgs:\n queue_id: The queue ID to retrieve parameters for\n\nReturns:\n A dictionary containing all stored recall parameters", + "operationId": "get_recall_parameters", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "queue_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The queue id to retrieve parameters for", + "title": "Queue Id" + }, + "description": "The queue id to retrieve parameters for" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Get Recall Parameters" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/custom_nodes/": { + "get": { + "tags": ["custom_nodes"], + "summary": "List Custom Node Packs", + "description": "Lists all installed custom node packs.\n\nAdmin-only: the response includes absolute filesystem paths, and non-admins have no\nlegitimate use for pack management data (install/uninstall/reload are also admin-only).", + "operationId": "list_custom_node_packs", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NodePackListResponse" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v2/custom_nodes/install": { + "post": { + "tags": ["custom_nodes"], + "summary": "Install Custom Node Pack", + "description": "Installs a custom node pack from a git URL by cloning it into the nodes directory.", + "operationId": "install_custom_node_pack", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstallNodePackRequest", + "description": "The source URL to install from." + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstallNodePackResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/api/v2/custom_nodes/{pack_name}": { + "delete": { + "tags": ["custom_nodes"], + "summary": "Uninstall Custom Node Pack", + "description": "Uninstalls a custom node pack by removing its directory.\n\nNote: A restart is required for the node removal to take full effect.\nInstalled nodes from the pack will remain registered until restart.", + "operationId": "uninstall_custom_node_pack", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "pack_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Pack Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UninstallNodePackResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/custom_nodes/reload": { + "post": { + "tags": ["custom_nodes"], + "summary": "Reload Custom Nodes", + "description": "Triggers a reload of all custom nodes.\n\nThis re-scans the nodes directory and loads any new node packs.\nAlready loaded packs are skipped.", + "operationId": "reload_custom_nodes", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Response Reload Custom Nodes" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + } + }, + "components": { + "schemas": { + "AddImagesToBoardResult": { + "properties": { + "affected_boards": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Affected Boards", + "description": "The ids of boards affected by the delete operation" + }, + "added_images": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Added Images", + "description": "The image names that were added to the board" + } + }, + "type": "object", + "required": ["affected_boards", "added_images"], + "title": "AddImagesToBoardResult" + }, + "AddInvocation": { + "category": "math", + "class": "invocation", + "classification": "stable", + "description": "Adds two numbers", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "a": { + "default": 0, + "description": "The first number", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "A", + "type": "integer" + }, + "b": { + "default": 0, + "description": "The second number", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "B", + "type": "integer" + }, + "type": { + "const": "add", + "default": "add", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["math", "add"], + "title": "Add Integers", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/IntegerOutput" + } + }, + "AdminUserCreateRequest": { + "properties": { + "email": { + "type": "string", + "title": "Email", + "description": "User email address" + }, + "display_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Display Name", + "description": "Display name" + }, + "password": { + "type": "string", + "title": "Password", + "description": "User password" + }, + "is_admin": { + "type": "boolean", + "title": "Is Admin", + "description": "Whether user should have admin privileges", + "default": false + } + }, + "type": "object", + "required": ["email", "password"], + "title": "AdminUserCreateRequest", + "description": "Request body for admin to create a new user." + }, + "AdminUserUpdateRequest": { + "properties": { + "display_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Display Name", + "description": "Display name" + }, + "password": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Password", + "description": "New password" + }, + "is_admin": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Admin", + "description": "Whether user should have admin privileges" + }, + "is_active": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Active", + "description": "Whether user account should be active" + } + }, + "type": "object", + "title": "AdminUserUpdateRequest", + "description": "Request body for admin to update any user." + }, + "AlibabaCloudImageGenerationInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Generate images using an Alibaba Cloud DashScope external model.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Main model (UNet, VAE, CLIP) to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "ui_model_base": ["external"], + "ui_model_format": ["external_api"], + "ui_model_provider_id": ["alibabacloud"], + "ui_model_type": ["external_image_generator"] + }, + "mode": { + "default": "txt2img", + "description": "Generation mode. Not all modes are supported by every model; unsupported modes raise at runtime.", + "enum": ["txt2img", "img2img", "inpaint"], + "field_kind": "input", + "input": "any", + "orig_default": "txt2img", + "orig_required": false, + "title": "Mode", + "type": "string" + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Prompt", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Prompt" + }, + "seed": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Seed for random number generation", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Seed" + }, + "num_images": { + "default": 1, + "description": "Number of images to generate", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 1, + "orig_required": false, + "title": "Num Images", + "type": "integer" + }, + "width": { + "default": 1024, + "description": "Width of output (px)", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 1024, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 1024, + "description": "Height of output (px)", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 1024, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "image_size": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Image size preset (e.g. 1K, 2K, 4K)", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Image Size" + }, + "init_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Init image for img2img/inpaint", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "mask_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Mask image for inpaint", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "reference_images": { + "default": [], + "description": "Reference images", + "field_kind": "input", + "input": "any", + "items": { + "$ref": "#/components/schemas/ImageField" + }, + "orig_default": [], + "orig_required": false, + "title": "Reference Images", + "type": "array" + }, + "type": { + "const": "alibabacloud_image_generation", + "default": "alibabacloud_image_generation", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["external", "generation", "alibabacloud", "dashscope"], + "title": "Alibaba Cloud DashScope Image Generation", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageCollectionOutput" + } + }, + "AlphaMaskToTensorInvocation": { + "category": "mask", + "class": "invocation", + "classification": "stable", + "description": "Convert a mask image to a tensor. Opaque regions are 1 and transparent regions are 0.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The mask image to convert.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "invert": { + "default": false, + "description": "Whether to invert the mask.", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Invert", + "type": "boolean" + }, + "type": { + "const": "alpha_mask_to_tensor", + "default": "alpha_mask_to_tensor", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["conditioning"], + "title": "Alpha Mask to Tensor", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/MaskOutput" + } + }, + "AnimaConditioningField": { + "description": "An Anima conditioning tensor primitive value.\n\nAnima conditioning contains Qwen3 0.6B hidden states and T5-XXL token IDs,\nwhich are combined by the LLM Adapter inside the transformer.", + "properties": { + "conditioning_name": { + "description": "The name of conditioning tensor", + "title": "Conditioning Name", + "type": "string" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "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." + } + }, + "required": ["conditioning_name"], + "title": "AnimaConditioningField", + "type": "object" + }, + "AnimaConditioningOutput": { + "class": "output", + "description": "Base class for nodes that output an Anima text conditioning tensor.", + "properties": { + "conditioning": { + "$ref": "#/components/schemas/AnimaConditioningField", + "description": "Conditioning tensor", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "anima_conditioning_output", + "default": "anima_conditioning_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "conditioning", "type", "type"], + "title": "AnimaConditioningOutput", + "type": "object" + }, + "AnimaDenoiseInvocation": { + "category": "image", + "class": "invocation", + "classification": "prototype", + "description": "Run the denoising process with an Anima model.\n\nUses rectified flow sampling with shift=3.0 and the Cosmos Predict2 DiT\nbackbone with integrated LLM Adapter for text conditioning.\n\nSupports txt2img, img2img (via latents input), and inpainting (via denoise_mask).", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "noise": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Noise tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoise_mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/DenoiseMaskField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "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.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoising_start": { + "default": 0.0, + "description": "When to start denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0.0, + "orig_required": false, + "title": "Denoising Start", + "type": "number" + }, + "denoising_end": { + "default": 1.0, + "description": "When to stop denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Denoising End", + "type": "number" + }, + "add_noise": { + "default": true, + "description": "Add noise based on denoising start.", + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "Add Noise", + "type": "boolean" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Anima transformer model.", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Transformer" + }, + "positive_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/AnimaConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/AnimaConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Positive conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Positive Conditioning" + }, + "negative_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/AnimaConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/AnimaConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Negative conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Negative Conditioning" + }, + "guidance_scale": { + "default": 4.5, + "description": "Guidance scale for classifier-free guidance. Recommended: 4.0-5.0 for Anima.", + "field_kind": "input", + "input": "any", + "minimum": 1.0, + "orig_default": 4.5, + "orig_required": false, + "title": "Guidance Scale", + "type": "number" + }, + "width": { + "default": 1024, + "description": "Width of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 8, + "orig_default": 1024, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 1024, + "description": "Height of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 8, + "orig_default": 1024, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "steps": { + "default": 30, + "description": "Number of denoising steps. 30 recommended for Anima.", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 30, + "orig_required": false, + "title": "Steps", + "type": "integer" + }, + "seed": { + "default": 0, + "description": "Randomness seed for reproducibility.", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Seed", + "type": "integer" + }, + "scheduler": { + "default": "euler", + "description": "Scheduler (sampler) for the denoising process.", + "enum": ["euler", "heun", "dpmpp_2m", "dpmpp_2m_sde", "er_sde", "lcm"], + "field_kind": "input", + "input": "any", + "orig_default": "euler", + "orig_required": false, + "title": "Scheduler", + "type": "string", + "ui_choice_labels": { + "dpmpp_2m": "DPM++ 2M", + "dpmpp_2m_sde": "DPM++ 2M SDE", + "er_sde": "ER-SDE", + "euler": "Euler", + "heun": "Heun (2nd order)", + "lcm": "LCM" + } + }, + "type": { + "const": "anima_denoise", + "default": "anima_denoise", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "anima"], + "title": "Denoise - Anima", + "type": "object", + "version": "1.6.0", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "AnimaImageToLatentsInvocation": { + "category": "image", + "class": "invocation", + "classification": "prototype", + "description": "Generates latents from an image using the Anima VAE (supports Wan 2.1 and FLUX VAE).", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to encode.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "type": { + "const": "anima_i2l", + "default": "anima_i2l", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "latents", "vae", "i2l", "anima"], + "title": "Image to Latents - Anima", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "AnimaLatentsToImageInvocation": { + "category": "latents", + "class": "invocation", + "classification": "prototype", + "description": "Generates an image from latents using the Anima VAE.\n\nSupports the Wan 2.1 QwenImage VAE (AutoencoderKLWan) with explicit\nlatent denormalization, and FLUX VAE as fallback.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "type": { + "const": "anima_l2i", + "default": "anima_l2i", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "image", "vae", "l2i", "anima"], + "title": "Latents to Image - Anima", + "type": "object", + "version": "1.0.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "AnimaLoRACollectionLoader": { + "category": "model", + "class": "invocation", + "classification": "prototype", + "description": "Applies a collection of LoRAs to an Anima transformer.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "loras": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoRAField" + }, + { + "items": { + "$ref": "#/components/schemas/LoRAField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LoRA models and weights. May be a single LoRA or collection.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "LoRAs" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Transformer", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Transformer" + }, + "qwen3_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/Qwen3EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Qwen3 tokenizer and text encoder", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Qwen3 Encoder" + }, + "type": { + "const": "anima_lora_collection_loader", + "default": "anima_lora_collection_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["lora", "model", "anima"], + "title": "Apply LoRA Collection - Anima", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/AnimaLoRALoaderOutput" + } + }, + "AnimaLoRALoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "prototype", + "description": "Apply a LoRA model to an Anima transformer and/or Qwen3 text encoder.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "lora": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LoRA model to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "LoRA", + "ui_model_base": ["anima"], + "ui_model_type": ["lora"] + }, + "weight": { + "default": 0.75, + "description": "The weight at which the LoRA is applied to each model", + "field_kind": "input", + "input": "any", + "orig_default": 0.75, + "orig_required": false, + "title": "Weight", + "type": "number" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Transformer", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Anima Transformer" + }, + "qwen3_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/Qwen3EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Qwen3 tokenizer and text encoder", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Qwen3 Encoder" + }, + "type": { + "const": "anima_lora_loader", + "default": "anima_lora_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["lora", "model", "anima"], + "title": "Apply LoRA - Anima", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/AnimaLoRALoaderOutput" + } + }, + "AnimaLoRALoaderOutput": { + "class": "output", + "description": "Anima LoRA Loader Output", + "properties": { + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Transformer", + "field_kind": "output", + "title": "Anima Transformer", + "ui_hidden": false + }, + "qwen3_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/Qwen3EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Qwen3 tokenizer and text encoder", + "field_kind": "output", + "title": "Qwen3 Encoder", + "ui_hidden": false + }, + "type": { + "const": "anima_lora_loader_output", + "default": "anima_lora_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "transformer", "qwen3_encoder", "type", "type"], + "title": "AnimaLoRALoaderOutput", + "type": "object" + }, + "AnimaModelLoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "prototype", + "description": "Loads an Anima model, outputting its submodels.\n\nAnima uses:\n- Transformer: Cosmos Predict2 DiT + LLM Adapter (from single-file checkpoint)\n- Qwen3 Encoder: Qwen3 0.6B (standalone single-file)\n- VAE: AutoencoderKLQwenImage / Wan 2.1 VAE (standalone single-file or FLUX VAE)\n\nThe T5-XXL tokenizer needed for LLM Adapter token IDs is bundled in the package,\nso no T5-XXL encoder model needs to be installed.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Anima main model (transformer + LLM adapter).", + "field_kind": "input", + "input": "direct", + "orig_required": true, + "title": "Transformer", + "ui_model_base": ["anima"], + "ui_model_type": ["main"] + }, + "vae_model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "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.", + "field_kind": "input", + "input": "direct", + "orig_required": true, + "title": "VAE", + "ui_model_type": ["vae"] + }, + "qwen3_encoder_model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Standalone Qwen3 0.6B Encoder model.", + "field_kind": "input", + "input": "direct", + "orig_required": true, + "title": "Qwen3 Encoder", + "ui_model_type": ["qwen3_encoder"] + }, + "type": { + "const": "anima_model_loader", + "default": "anima_model_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["model", "vae_model", "qwen3_encoder_model", "type", "id"], + "tags": ["model", "anima"], + "title": "Main Model - Anima", + "type": "object", + "version": "1.4.0", + "output": { + "$ref": "#/components/schemas/AnimaModelLoaderOutput" + } + }, + "AnimaModelLoaderOutput": { + "class": "output", + "description": "Anima model loader output.", + "properties": { + "transformer": { + "$ref": "#/components/schemas/TransformerField", + "description": "Transformer", + "field_kind": "output", + "title": "Transformer", + "ui_hidden": false + }, + "qwen3_encoder": { + "$ref": "#/components/schemas/Qwen3EncoderField", + "description": "Qwen3 tokenizer and text encoder", + "field_kind": "output", + "title": "Qwen3 Encoder", + "ui_hidden": false + }, + "vae": { + "$ref": "#/components/schemas/VAEField", + "description": "VAE", + "field_kind": "output", + "title": "VAE", + "ui_hidden": false + }, + "type": { + "const": "anima_model_loader_output", + "default": "anima_model_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "transformer", "qwen3_encoder", "vae", "type", "type"], + "title": "AnimaModelLoaderOutput", + "type": "object" + }, + "AnimaTextEncoderInvocation": { + "category": "conditioning", + "class": "invocation", + "classification": "prototype", + "description": "Encodes and preps a prompt for an Anima image.\n\nUses Qwen3 0.6B for hidden state extraction and a bundled T5-XXL tokenizer for\ntoken IDs (no T5 model weights needed). Both are combined by the\nLLM Adapter inside the Anima transformer during denoising.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Text prompt to encode.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Prompt", + "ui_component": "textarea" + }, + "qwen3_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/Qwen3EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Qwen3 tokenizer and text encoder", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Qwen3 Encoder" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A mask defining the region that this conditioning prompt applies to.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "type": { + "const": "anima_text_encoder", + "default": "anima_text_encoder", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["prompt", "conditioning", "anima"], + "title": "Prompt - Anima", + "type": "object", + "version": "1.4.0", + "output": { + "$ref": "#/components/schemas/AnimaConditioningOutput" + } + }, + "AnyModelConfig": { + "oneOf": [ + { + "$ref": "#/components/schemas/Main_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD3_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_ZImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SDXL_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Spandrel_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config" + }, + { + "$ref": "#/components/schemas/CLIPVision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/SigLIP_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/TextLLM_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/ExternalApiModelConfig" + }, + { + "$ref": "#/components/schemas/Unknown_Config" + } + ] + }, + "AppVersion": { + "properties": { + "version": { + "type": "string", + "title": "Version", + "description": "App version" + } + }, + "type": "object", + "required": ["version"], + "title": "AppVersion", + "description": "App Version Response" + }, + "ApplyMaskTensorToImageInvocation": { + "category": "mask", + "class": "invocation", + "classification": "stable", + "description": "Applies a tensor mask to an image.\n\nThe image is converted to RGBA and the mask is applied to the alpha channel.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The mask tensor to apply.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to apply the mask to.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "invert": { + "default": false, + "description": "Whether to invert the mask.", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Invert", + "type": "boolean" + }, + "type": { + "const": "apply_tensor_mask_to_image", + "default": "apply_tensor_mask_to_image", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["mask"], + "title": "Apply Tensor Mask to Image", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ApplyMaskToImageInvocation": { + "category": "mask", + "class": "invocation", + "classification": "stable", + "description": "Extracts a region from a generated image using a mask and blends it seamlessly onto a source image.\nThe mask uses black to indicate areas to keep from the generated image and white for areas to discard.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image from which to extract the masked region", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The mask defining the region (black=keep, white=discard)", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "invert_mask": { + "default": false, + "description": "Whether to invert the mask before applying it", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Invert Mask", + "type": "boolean" + }, + "type": { + "const": "apply_mask_to_image", + "default": "apply_mask_to_image", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "mask", "blend"], + "title": "Apply Mask to Image", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "BaseMetadata": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "model's name" + }, + "type": { + "type": "string", + "const": "basemetadata", + "title": "Type", + "default": "basemetadata" + } + }, + "type": "object", + "required": ["name"], + "title": "BaseMetadata", + "description": "Adds typing data for discriminated union." + }, + "BaseModelType": { + "type": "string", + "enum": [ + "any", + "sd-1", + "sd-2", + "sd-3", + "sdxl", + "sdxl-refiner", + "flux", + "flux2", + "cogview4", + "z-image", + "external", + "qwen-image", + "anima", + "unknown" + ], + "title": "BaseModelType", + "description": "An enumeration of base model architectures. For example, Stable Diffusion 1.x, Stable Diffusion 2.x, FLUX, etc.\n\nEvery model config must have a base architecture type.\n\nNot all models are associated with a base architecture. For example, CLIP models are their own thing, not related\nto any particular model architecture. To simplify internal APIs and make it easier to work with models, we use a\nfallback/null value `BaseModelType.Any` for these models, instead of making the model base optional." + }, + "Batch": { + "properties": { + "batch_id": { + "type": "string", + "title": "Batch Id", + "description": "The ID of the batch" + }, + "origin": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Origin", + "description": "The origin of this queue item. This data is used by the frontend to determine how to handle results." + }, + "destination": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Destination", + "description": "The origin of this queue item. This data is used by the frontend to determine how to handle results" + }, + "data": { + "anyOf": [ + { + "items": { + "items": { + "$ref": "#/components/schemas/BatchDatum" + }, + "type": "array" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Data", + "description": "The batch data collection." + }, + "graph": { + "$ref": "#/components/schemas/Graph", + "description": "The graph to initialize the session with" + }, + "workflow": { + "anyOf": [ + { + "$ref": "#/components/schemas/WorkflowWithoutID" + }, + { + "type": "null" + } + ], + "description": "The workflow to initialize the session with" + }, + "runs": { + "type": "integer", + "minimum": 1.0, + "title": "Runs", + "description": "Int stating how many times to iterate through all possible batch indices", + "default": 1 + } + }, + "type": "object", + "required": ["graph", "runs"], + "title": "Batch" + }, + "BatchDatum": { + "properties": { + "node_path": { + "type": "string", + "title": "Node Path", + "description": "The node into which this batch data collection will be substituted." + }, + "field_name": { + "type": "string", + "title": "Field Name", + "description": "The field into which this batch data collection will be substituted." + }, + "items": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "integer" + }, + { + "$ref": "#/components/schemas/ImageField" + } + ] + }, + "type": "array", + "title": "Items", + "description": "The list of items to substitute into the node/field." + } + }, + "type": "object", + "required": ["node_path", "field_name"], + "title": "BatchDatum" + }, + "BatchEnqueuedEvent": { + "description": "Event model for batch_enqueued", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "queue_id": { + "description": "The ID of the queue", + "title": "Queue Id", + "type": "string" + }, + "batch_id": { + "description": "The ID of the batch", + "title": "Batch Id", + "type": "string" + }, + "enqueued": { + "description": "The number of invocations enqueued", + "title": "Enqueued", + "type": "integer" + }, + "requested": { + "description": "The number of invocations initially requested to be enqueued (may be less than enqueued if queue was full)", + "title": "Requested", + "type": "integer" + }, + "priority": { + "description": "The priority of the batch", + "title": "Priority", + "type": "integer" + }, + "origin": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The origin of the batch", + "title": "Origin" + }, + "user_id": { + "default": "system", + "description": "The ID of the user who enqueued the batch", + "title": "User Id", + "type": "string" + } + }, + "required": ["timestamp", "queue_id", "batch_id", "enqueued", "requested", "priority", "origin", "user_id"], + "title": "BatchEnqueuedEvent", + "type": "object" + }, + "BatchStatus": { + "properties": { + "queue_id": { + "type": "string", + "title": "Queue Id", + "description": "The ID of the queue" + }, + "batch_id": { + "type": "string", + "title": "Batch Id", + "description": "The ID of the batch" + }, + "origin": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Origin", + "description": "The origin of the batch" + }, + "destination": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Destination", + "description": "The destination of the batch" + }, + "pending": { + "type": "integer", + "title": "Pending", + "description": "Number of queue items with status 'pending'" + }, + "in_progress": { + "type": "integer", + "title": "In Progress", + "description": "Number of queue items with status 'in_progress'" + }, + "completed": { + "type": "integer", + "title": "Completed", + "description": "Number of queue items with status 'complete'" + }, + "failed": { + "type": "integer", + "title": "Failed", + "description": "Number of queue items with status 'error'" + }, + "canceled": { + "type": "integer", + "title": "Canceled", + "description": "Number of queue items with status 'canceled'" + }, + "total": { + "type": "integer", + "title": "Total", + "description": "Total number of queue items" + } + }, + "type": "object", + "required": [ + "queue_id", + "batch_id", + "origin", + "destination", + "pending", + "in_progress", + "completed", + "failed", + "canceled", + "total" + ], + "title": "BatchStatus" + }, + "BlankImageInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Creates a blank image and forwards it to the pipeline", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "width": { + "default": 512, + "description": "The width of the image", + "field_kind": "input", + "input": "any", + "orig_default": 512, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 512, + "description": "The height of the image", + "field_kind": "input", + "input": "any", + "orig_default": 512, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "mode": { + "default": "RGB", + "description": "The mode of the image", + "enum": ["RGB", "RGBA"], + "field_kind": "input", + "input": "any", + "orig_default": "RGB", + "orig_required": false, + "title": "Mode", + "type": "string" + }, + "color": { + "$ref": "#/components/schemas/ColorField", + "default": { + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "description": "The color of the image", + "field_kind": "input", + "input": "any", + "orig_default": { + "a": 255, + "b": 0, + "g": 0, + "r": 0 + }, + "orig_required": false + }, + "type": { + "const": "blank_image", + "default": "blank_image", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image"], + "title": "Blank Image", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "BlendLatentsInvocation": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Blend two latents using a given alpha. If a mask is provided, the second latents will be masked before blending.\nLatents must have same size. Masking functionality added by @dwringer.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents_a": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "latents_b": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Mask for blending in latents B", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "alpha": { + "default": 0.5, + "description": "Blending factor. 0.0 = use input A only, 1.0 = use input B only, 0.5 = 50% mix of input A and input B.", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 0.5, + "orig_required": false, + "title": "Alpha", + "type": "number" + }, + "type": { + "const": "lblend", + "default": "lblend", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "blend", "mask"], + "title": "Blend Latents", + "type": "object", + "version": "1.1.0", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "BoardChanges": { + "properties": { + "board_name": { + "anyOf": [ + { + "type": "string", + "maxLength": 300 + }, + { + "type": "null" + } + ], + "title": "Board Name", + "description": "The board's new name." + }, + "cover_image_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image Name", + "description": "The name of the board's new cover image." + }, + "archived": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Archived", + "description": "Whether or not the board is archived" + }, + "board_visibility": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardVisibility" + }, + { + "type": "null" + } + ], + "description": "The visibility of the board." + } + }, + "additionalProperties": false, + "type": "object", + "title": "BoardChanges" + }, + "BoardDTO": { + "properties": { + "board_id": { + "type": "string", + "title": "Board Id", + "description": "The unique ID of the board." + }, + "board_name": { + "type": "string", + "title": "Board Name", + "description": "The name of the board." + }, + "user_id": { + "type": "string", + "title": "User Id", + "description": "The user ID of the board owner." + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + } + ], + "title": "Created At", + "description": "The created timestamp of the board." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + } + ], + "title": "Updated At", + "description": "The updated timestamp of the board." + }, + "deleted_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Deleted At", + "description": "The deleted timestamp of the board." + }, + "cover_image_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image Name", + "description": "The name of the board's cover image." + }, + "archived": { + "type": "boolean", + "title": "Archived", + "description": "Whether or not the board is archived." + }, + "board_visibility": { + "$ref": "#/components/schemas/BoardVisibility", + "description": "The visibility of the board.", + "default": "private" + }, + "image_count": { + "type": "integer", + "title": "Image Count", + "description": "The number of images in the board." + }, + "asset_count": { + "type": "integer", + "title": "Asset Count", + "description": "The number of assets in the board." + }, + "owner_username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Owner Username", + "description": "The username of the board owner (for admin view)." + } + }, + "type": "object", + "required": [ + "board_id", + "board_name", + "user_id", + "created_at", + "updated_at", + "cover_image_name", + "archived", + "image_count", + "asset_count" + ], + "title": "BoardDTO", + "description": "Deserialized board record with cover image URL and image count." + }, + "BoardField": { + "description": "A board primitive field", + "properties": { + "board_id": { + "description": "The id of the board", + "title": "Board Id", + "type": "string" + } + }, + "required": ["board_id"], + "title": "BoardField", + "type": "object" + }, + "BoardRecordOrderBy": { + "type": "string", + "enum": ["created_at", "board_name"], + "title": "BoardRecordOrderBy", + "description": "The order by options for board records" + }, + "BoardVisibility": { + "type": "string", + "enum": ["private", "shared", "public"], + "title": "BoardVisibility", + "description": "The visibility options for a board." + }, + "Body_add_image_to_board": { + "properties": { + "board_id": { + "type": "string", + "title": "Board Id", + "description": "The id of the board to add to" + }, + "image_name": { + "type": "string", + "title": "Image Name", + "description": "The name of the image to add" + } + }, + "type": "object", + "required": ["board_id", "image_name"], + "title": "Body_add_image_to_board" + }, + "Body_add_images_to_board": { + "properties": { + "board_id": { + "type": "string", + "title": "Board Id", + "description": "The id of the board to add to" + }, + "image_names": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Image Names", + "description": "The names of the images to add" + } + }, + "type": "object", + "required": ["board_id", "image_names"], + "title": "Body_add_images_to_board" + }, + "Body_cancel_by_batch_ids": { + "properties": { + "batch_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Batch Ids", + "description": "The list of batch_ids to cancel all queue items for" + } + }, + "type": "object", + "required": ["batch_ids"], + "title": "Body_cancel_by_batch_ids" + }, + "Body_create_image_upload_entry": { + "properties": { + "width": { + "type": "integer", + "title": "Width", + "description": "The width of the image" + }, + "height": { + "type": "integer", + "title": "Height", + "description": "The height of the image" + }, + "board_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Board Id", + "description": "The board to add this image to, if any" + } + }, + "type": "object", + "required": ["width", "height"], + "title": "Body_create_image_upload_entry" + }, + "Body_create_style_preset": { + "properties": { + "image": { + "anyOf": [ + { + "type": "string", + "format": "binary" + }, + { + "type": "null" + } + ], + "title": "Image", + "description": "The image file to upload" + }, + "data": { + "type": "string", + "title": "Data", + "description": "The data of the style preset to create" + } + }, + "type": "object", + "required": ["data"], + "title": "Body_create_style_preset" + }, + "Body_create_workflow": { + "properties": { + "workflow": { + "$ref": "#/components/schemas/WorkflowWithoutID", + "description": "The workflow to create" + } + }, + "type": "object", + "required": ["workflow"], + "title": "Body_create_workflow" + }, + "Body_delete_images_from_list": { + "properties": { + "image_names": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Image Names", + "description": "The list of names of images to delete" + } + }, + "type": "object", + "required": ["image_names"], + "title": "Body_delete_images_from_list" + }, + "Body_do_hf_login": { + "properties": { + "token": { + "type": "string", + "title": "Token", + "description": "Hugging Face token to use for login" + } + }, + "type": "object", + "required": ["token"], + "title": "Body_do_hf_login" + }, + "Body_download": { + "properties": { + "source": { + "type": "string", + "minLength": 1, + "format": "uri", + "title": "Source", + "description": "download source" + }, + "dest": { + "type": "string", + "title": "Dest", + "description": "download destination" + }, + "priority": { + "type": "integer", + "title": "Priority", + "description": "queue priority", + "default": 10 + }, + "access_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Access Token", + "description": "token for authorization to download" + } + }, + "type": "object", + "required": ["source", "dest"], + "title": "Body_download" + }, + "Body_download_images_from_list": { + "properties": { + "image_names": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Image Names", + "description": "The list of names of images to download" + }, + "board_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Board Id", + "description": "The board from which image should be downloaded" + } + }, + "type": "object", + "title": "Body_download_images_from_list" + }, + "Body_enqueue_batch": { + "properties": { + "batch": { + "$ref": "#/components/schemas/Batch", + "description": "Batch to process" + }, + "prepend": { + "type": "boolean", + "title": "Prepend", + "description": "Whether or not to prepend this batch in the queue", + "default": false + } + }, + "type": "object", + "required": ["batch"], + "title": "Body_enqueue_batch" + }, + "Body_get_images_by_names": { + "properties": { + "image_names": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Image Names", + "description": "Object containing list of image names to fetch DTOs for" + } + }, + "type": "object", + "required": ["image_names"], + "title": "Body_get_images_by_names" + }, + "Body_get_queue_items_by_item_ids": { + "properties": { + "item_ids": { + "items": { + "type": "integer" + }, + "type": "array", + "title": "Item Ids", + "description": "Object containing list of queue item ids to fetch queue items for" + } + }, + "type": "object", + "required": ["item_ids"], + "title": "Body_get_queue_items_by_item_ids" + }, + "Body_import_style_presets": { + "properties": { + "file": { + "type": "string", + "format": "binary", + "title": "File", + "description": "The file to import" + } + }, + "type": "object", + "required": ["file"], + "title": "Body_import_style_presets" + }, + "Body_parse_dynamicprompts": { + "properties": { + "prompt": { + "type": "string", + "title": "Prompt", + "description": "The prompt to parse with dynamicprompts" + }, + "max_prompts": { + "type": "integer", + "maximum": 10000.0, + "minimum": 1.0, + "title": "Max Prompts", + "description": "The max number of prompts to generate", + "default": 1000 + }, + "combinatorial": { + "type": "boolean", + "title": "Combinatorial", + "description": "Whether to use the combinatorial generator", + "default": true + }, + "seed": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Seed", + "description": "The seed to use for random generation. Only used if not combinatorial" + } + }, + "type": "object", + "required": ["prompt"], + "title": "Body_parse_dynamicprompts" + }, + "Body_remove_image_from_board": { + "properties": { + "image_name": { + "type": "string", + "title": "Image Name", + "description": "The name of the image to remove" + } + }, + "type": "object", + "required": ["image_name"], + "title": "Body_remove_image_from_board" + }, + "Body_remove_images_from_board": { + "properties": { + "image_names": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Image Names", + "description": "The names of the images to remove" + } + }, + "type": "object", + "required": ["image_names"], + "title": "Body_remove_images_from_board" + }, + "Body_set_workflow_thumbnail": { + "properties": { + "image": { + "type": "string", + "format": "binary", + "title": "Image", + "description": "The image file to upload" + } + }, + "type": "object", + "required": ["image"], + "title": "Body_set_workflow_thumbnail" + }, + "Body_star_images_in_list": { + "properties": { + "image_names": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Image Names", + "description": "The list of names of images to star" + } + }, + "type": "object", + "required": ["image_names"], + "title": "Body_star_images_in_list" + }, + "Body_unstar_images_in_list": { + "properties": { + "image_names": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Image Names", + "description": "The list of names of images to unstar" + } + }, + "type": "object", + "required": ["image_names"], + "title": "Body_unstar_images_in_list" + }, + "Body_update_model_image": { + "properties": { + "image": { + "type": "string", + "format": "binary", + "title": "Image" + } + }, + "type": "object", + "required": ["image"], + "title": "Body_update_model_image" + }, + "Body_update_style_preset": { + "properties": { + "image": { + "anyOf": [ + { + "type": "string", + "format": "binary" + }, + { + "type": "null" + } + ], + "title": "Image", + "description": "The image file to upload" + }, + "data": { + "type": "string", + "title": "Data", + "description": "The data of the style preset to update" + } + }, + "type": "object", + "required": ["data"], + "title": "Body_update_style_preset" + }, + "Body_update_workflow": { + "properties": { + "workflow": { + "$ref": "#/components/schemas/Workflow", + "description": "The updated workflow" + } + }, + "type": "object", + "required": ["workflow"], + "title": "Body_update_workflow" + }, + "Body_update_workflow_is_public": { + "properties": { + "is_public": { + "type": "boolean", + "title": "Is Public", + "description": "Whether the workflow should be shared publicly" + } + }, + "type": "object", + "required": ["is_public"], + "title": "Body_update_workflow_is_public" + }, + "Body_upload_image": { + "properties": { + "file": { + "type": "string", + "format": "binary", + "title": "File" + }, + "resize_to": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Resize To", + "description": "Dimensions to resize the image to, must be stringified tuple of 2 integers. Max total pixel count: 16777216", + "examples": ["\"[1024,1024]\""] + }, + "metadata": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "The metadata to associate with the image, must be a stringified JSON dict" + } + }, + "type": "object", + "required": ["file"], + "title": "Body_upload_image" + }, + "BooleanCollectionInvocation": { + "category": "primitives", + "class": "invocation", + "classification": "stable", + "description": "A collection of boolean primitive values", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "collection": { + "default": [], + "description": "The collection of boolean values", + "field_kind": "input", + "input": "any", + "items": { + "type": "boolean" + }, + "orig_default": [], + "orig_required": false, + "title": "Collection", + "type": "array" + }, + "type": { + "const": "boolean_collection", + "default": "boolean_collection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "boolean", "collection"], + "title": "Boolean Collection Primitive", + "type": "object", + "version": "1.0.2", + "output": { + "$ref": "#/components/schemas/BooleanCollectionOutput" + } + }, + "BooleanCollectionOutput": { + "class": "output", + "description": "Base class for nodes that output a collection of booleans", + "properties": { + "collection": { + "description": "The output boolean collection", + "field_kind": "output", + "items": { + "type": "boolean" + }, + "title": "Collection", + "type": "array", + "ui_hidden": false + }, + "type": { + "const": "boolean_collection_output", + "default": "boolean_collection_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "collection", "type", "type"], + "title": "BooleanCollectionOutput", + "type": "object" + }, + "BooleanInvocation": { + "category": "primitives", + "class": "invocation", + "classification": "stable", + "description": "A boolean primitive value", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "value": { + "default": false, + "description": "The boolean value", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Value", + "type": "boolean" + }, + "type": { + "const": "boolean", + "default": "boolean", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "boolean"], + "title": "Boolean Primitive", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/BooleanOutput" + } + }, + "BooleanOutput": { + "class": "output", + "description": "Base class for nodes that output a single boolean", + "properties": { + "value": { + "description": "The output boolean", + "field_kind": "output", + "title": "Value", + "type": "boolean", + "ui_hidden": false + }, + "type": { + "const": "boolean_output", + "default": "boolean_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "value", "type", "type"], + "title": "BooleanOutput", + "type": "object" + }, + "BoundingBoxCollectionOutput": { + "class": "output", + "description": "Base class for nodes that output a collection of bounding boxes", + "properties": { + "collection": { + "description": "The output bounding boxes.", + "field_kind": "output", + "items": { + "$ref": "#/components/schemas/BoundingBoxField" + }, + "title": "Bounding Boxes", + "type": "array", + "ui_hidden": false + }, + "type": { + "const": "bounding_box_collection_output", + "default": "bounding_box_collection_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "collection", "type", "type"], + "title": "BoundingBoxCollectionOutput", + "type": "object" + }, + "BoundingBoxField": { + "description": "A bounding box primitive value.", + "properties": { + "x_min": { + "description": "The minimum x-coordinate of the bounding box (inclusive).", + "title": "X Min", + "type": "integer" + }, + "x_max": { + "description": "The maximum x-coordinate of the bounding box (exclusive).", + "title": "X Max", + "type": "integer" + }, + "y_min": { + "description": "The minimum y-coordinate of the bounding box (inclusive).", + "title": "Y Min", + "type": "integer" + }, + "y_max": { + "description": "The maximum y-coordinate of the bounding box (exclusive).", + "title": "Y Max", + "type": "integer" + }, + "score": { + "anyOf": [ + { + "maximum": 1.0, + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "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.", + "title": "Score" + } + }, + "required": ["x_min", "x_max", "y_min", "y_max"], + "title": "BoundingBoxField", + "type": "object" + }, + "BoundingBoxInvocation": { + "category": "primitives", + "class": "invocation", + "classification": "stable", + "description": "Create a bounding box manually by supplying box coordinates", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "x_min": { + "default": 0, + "description": "x-coordinate of the bounding box's top left vertex", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "X Min", + "type": "integer" + }, + "y_min": { + "default": 0, + "description": "y-coordinate of the bounding box's top left vertex", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Y Min", + "type": "integer" + }, + "x_max": { + "default": 0, + "description": "x-coordinate of the bounding box's bottom right vertex", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "X Max", + "type": "integer" + }, + "y_max": { + "default": 0, + "description": "y-coordinate of the bounding box's bottom right vertex", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Y Max", + "type": "integer" + }, + "type": { + "const": "bounding_box", + "default": "bounding_box", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "segmentation", "collection", "bounding box"], + "title": "Bounding Box", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/BoundingBoxOutput" + } + }, + "BoundingBoxOutput": { + "class": "output", + "description": "Base class for nodes that output a single bounding box", + "properties": { + "bounding_box": { + "$ref": "#/components/schemas/BoundingBoxField", + "description": "The output bounding box.", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "bounding_box_output", + "default": "bounding_box_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "bounding_box", "type", "type"], + "title": "BoundingBoxOutput", + "type": "object" + }, + "BulkDeleteModelsRequest": { + "properties": { + "keys": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Keys", + "description": "List of model keys to delete" + } + }, + "type": "object", + "required": ["keys"], + "title": "BulkDeleteModelsRequest", + "description": "Request body for bulk model deletion." + }, + "BulkDeleteModelsResponse": { + "properties": { + "deleted": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Deleted", + "description": "List of successfully deleted model keys" + }, + "failed": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array", + "title": "Failed", + "description": "List of failed deletions with error messages" + } + }, + "type": "object", + "required": ["deleted", "failed"], + "title": "BulkDeleteModelsResponse", + "description": "Response body for bulk model deletion." + }, + "BulkDownloadCompleteEvent": { + "description": "Event model for bulk_download_complete", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "bulk_download_id": { + "description": "The ID of the bulk image download", + "title": "Bulk Download Id", + "type": "string" + }, + "bulk_download_item_id": { + "description": "The ID of the bulk image download item", + "title": "Bulk Download Item Id", + "type": "string" + }, + "bulk_download_item_name": { + "description": "The name of the bulk image download item", + "title": "Bulk Download Item Name", + "type": "string" + }, + "user_id": { + "default": "system", + "description": "The ID of the user who initiated the download", + "title": "User Id", + "type": "string" + } + }, + "required": ["timestamp", "bulk_download_id", "bulk_download_item_id", "bulk_download_item_name", "user_id"], + "title": "BulkDownloadCompleteEvent", + "type": "object" + }, + "BulkDownloadErrorEvent": { + "description": "Event model for bulk_download_error", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "bulk_download_id": { + "description": "The ID of the bulk image download", + "title": "Bulk Download Id", + "type": "string" + }, + "bulk_download_item_id": { + "description": "The ID of the bulk image download item", + "title": "Bulk Download Item Id", + "type": "string" + }, + "bulk_download_item_name": { + "description": "The name of the bulk image download item", + "title": "Bulk Download Item Name", + "type": "string" + }, + "user_id": { + "default": "system", + "description": "The ID of the user who initiated the download", + "title": "User Id", + "type": "string" + }, + "error": { + "description": "The error message", + "title": "Error", + "type": "string" + } + }, + "required": [ + "timestamp", + "bulk_download_id", + "bulk_download_item_id", + "bulk_download_item_name", + "user_id", + "error" + ], + "title": "BulkDownloadErrorEvent", + "type": "object" + }, + "BulkDownloadStartedEvent": { + "description": "Event model for bulk_download_started", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "bulk_download_id": { + "description": "The ID of the bulk image download", + "title": "Bulk Download Id", + "type": "string" + }, + "bulk_download_item_id": { + "description": "The ID of the bulk image download item", + "title": "Bulk Download Item Id", + "type": "string" + }, + "bulk_download_item_name": { + "description": "The name of the bulk image download item", + "title": "Bulk Download Item Name", + "type": "string" + }, + "user_id": { + "default": "system", + "description": "The ID of the user who initiated the download", + "title": "User Id", + "type": "string" + } + }, + "required": ["timestamp", "bulk_download_id", "bulk_download_item_id", "bulk_download_item_name", "user_id"], + "title": "BulkDownloadStartedEvent", + "type": "object" + }, + "BulkReidentifyModelsRequest": { + "properties": { + "keys": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Keys", + "description": "List of model keys to reidentify" + } + }, + "type": "object", + "required": ["keys"], + "title": "BulkReidentifyModelsRequest", + "description": "Request body for bulk model reidentification." + }, + "BulkReidentifyModelsResponse": { + "properties": { + "succeeded": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Succeeded", + "description": "List of successfully reidentified model keys" + }, + "failed": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array", + "title": "Failed", + "description": "List of failed reidentifications with error messages" + } + }, + "type": "object", + "required": ["succeeded", "failed"], + "title": "BulkReidentifyModelsResponse", + "description": "Response body for bulk model reidentification." + }, + "CLIPEmbed_Diffusers_G_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "base": { + "type": "string", + "const": "any", + "title": "Base", + "default": "any" + }, + "type": { + "type": "string", + "const": "clip_embed", + "title": "Type", + "default": "clip_embed" + }, + "cpu_only": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Cpu Only", + "description": "Whether this model should run on CPU only" + }, + "variant": { + "type": "string", + "const": "gigantic", + "title": "Variant", + "default": "gigantic" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "format", + "repo_variant", + "base", + "type", + "cpu_only", + "variant" + ], + "title": "CLIPEmbed_Diffusers_G_Config" + }, + "CLIPEmbed_Diffusers_L_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "base": { + "type": "string", + "const": "any", + "title": "Base", + "default": "any" + }, + "type": { + "type": "string", + "const": "clip_embed", + "title": "Type", + "default": "clip_embed" + }, + "cpu_only": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Cpu Only", + "description": "Whether this model should run on CPU only" + }, + "variant": { + "type": "string", + "const": "large", + "title": "Variant", + "default": "large" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "format", + "repo_variant", + "base", + "type", + "cpu_only", + "variant" + ], + "title": "CLIPEmbed_Diffusers_L_Config" + }, + "CLIPField": { + "properties": { + "tokenizer": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Info to load tokenizer submodel" + }, + "text_encoder": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Info to load text_encoder submodel" + }, + "skipped_layers": { + "description": "Number of skipped layers in text_encoder", + "title": "Skipped Layers", + "type": "integer" + }, + "loras": { + "description": "LoRAs to apply on model loading", + "items": { + "$ref": "#/components/schemas/LoRAField" + }, + "title": "Loras", + "type": "array" + } + }, + "required": ["tokenizer", "text_encoder", "skipped_layers", "loras"], + "title": "CLIPField", + "type": "object" + }, + "CLIPOutput": { + "class": "output", + "description": "Base class for invocations that output a CLIP field", + "properties": { + "clip": { + "$ref": "#/components/schemas/CLIPField", + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "output", + "title": "CLIP", + "ui_hidden": false + }, + "type": { + "const": "clip_output", + "default": "clip_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "clip", "type", "type"], + "title": "CLIPOutput", + "type": "object" + }, + "CLIPSkipInvocation": { + "category": "prompt", + "class": "invocation", + "classification": "stable", + "description": "Skip layers in clip text_encoder model.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "clip": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "CLIP" + }, + "skipped_layers": { + "default": 0, + "description": "Number of layers to skip in text encoder", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Skipped Layers", + "type": "integer" + }, + "type": { + "const": "clip_skip", + "default": "clip_skip", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["clipskip", "clip", "skip"], + "title": "Apply CLIP Skip - SD1.5, SDXL", + "type": "object", + "version": "1.1.1", + "output": { + "$ref": "#/components/schemas/CLIPSkipInvocationOutput" + } + }, + "CLIPSkipInvocationOutput": { + "class": "output", + "description": "CLIP skip node output", + "properties": { + "clip": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "output", + "title": "CLIP", + "ui_hidden": false + }, + "type": { + "const": "clip_skip_output", + "default": "clip_skip_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "clip", "type", "type"], + "title": "CLIPSkipInvocationOutput", + "type": "object" + }, + "CLIPVision_Diffusers_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "base": { + "type": "string", + "const": "any", + "title": "Base", + "default": "any" + }, + "type": { + "type": "string", + "const": "clip_vision", + "title": "Type", + "default": "clip_vision" + }, + "cpu_only": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Cpu Only", + "description": "Whether this model should run on CPU only" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "format", + "repo_variant", + "base", + "type", + "cpu_only" + ], + "title": "CLIPVision_Diffusers_Config", + "description": "Model config for CLIPVision." + }, + "CV2InfillInvocation": { + "category": "inpaint", + "class": "invocation", + "classification": "stable", + "description": "Infills transparent areas of an image using OpenCV Inpainting", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "infill_cv2", + "default": "infill_cv2", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "inpaint"], + "title": "CV2 Infill", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "CacheStats": { + "properties": { + "hits": { + "type": "integer", + "title": "Hits", + "default": 0 + }, + "misses": { + "type": "integer", + "title": "Misses", + "default": 0 + }, + "high_watermark": { + "type": "integer", + "title": "High Watermark", + "default": 0 + }, + "in_cache": { + "type": "integer", + "title": "In Cache", + "default": 0 + }, + "cleared": { + "type": "integer", + "title": "Cleared", + "default": 0 + }, + "cache_size": { + "type": "integer", + "title": "Cache Size", + "default": 0 + }, + "loaded_model_sizes": { + "additionalProperties": { + "type": "integer" + }, + "type": "object", + "title": "Loaded Model Sizes" + } + }, + "type": "object", + "title": "CacheStats" + }, + "CalculateImageTilesEvenSplitInvocation": { + "category": "tiles", + "class": "invocation", + "classification": "stable", + "description": "Calculate the coordinates and overlaps of tiles that cover a target image shape.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image_width": { + "default": 1024, + "description": "The image width, in pixels, to calculate tiles for.", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 1024, + "orig_required": false, + "title": "Image Width", + "type": "integer" + }, + "image_height": { + "default": 1024, + "description": "The image height, in pixels, to calculate tiles for.", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 1024, + "orig_required": false, + "title": "Image Height", + "type": "integer" + }, + "num_tiles_x": { + "default": 2, + "description": "Number of tiles to divide image into on the x axis", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 2, + "orig_required": false, + "title": "Num Tiles X", + "type": "integer" + }, + "num_tiles_y": { + "default": 2, + "description": "Number of tiles to divide image into on the y axis", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 2, + "orig_required": false, + "title": "Num Tiles Y", + "type": "integer" + }, + "overlap": { + "default": 128, + "description": "The overlap, in pixels, between adjacent tiles.", + "field_kind": "input", + "input": "any", + "minimum": 0, + "multipleOf": 8, + "orig_default": 128, + "orig_required": false, + "title": "Overlap", + "type": "integer" + }, + "type": { + "const": "calculate_image_tiles_even_split", + "default": "calculate_image_tiles_even_split", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["tiles"], + "title": "Calculate Image Tiles Even Split", + "type": "object", + "version": "1.1.1", + "output": { + "$ref": "#/components/schemas/CalculateImageTilesOutput" + } + }, + "CalculateImageTilesInvocation": { + "category": "tiles", + "class": "invocation", + "classification": "stable", + "description": "Calculate the coordinates and overlaps of tiles that cover a target image shape.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image_width": { + "default": 1024, + "description": "The image width, in pixels, to calculate tiles for.", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 1024, + "orig_required": false, + "title": "Image Width", + "type": "integer" + }, + "image_height": { + "default": 1024, + "description": "The image height, in pixels, to calculate tiles for.", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 1024, + "orig_required": false, + "title": "Image Height", + "type": "integer" + }, + "tile_width": { + "default": 576, + "description": "The tile width, in pixels.", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 576, + "orig_required": false, + "title": "Tile Width", + "type": "integer" + }, + "tile_height": { + "default": 576, + "description": "The tile height, in pixels.", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 576, + "orig_required": false, + "title": "Tile Height", + "type": "integer" + }, + "overlap": { + "default": 128, + "description": "The target overlap, in pixels, between adjacent tiles. Adjacent tiles will overlap by at least this amount", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 128, + "orig_required": false, + "title": "Overlap", + "type": "integer" + }, + "type": { + "const": "calculate_image_tiles", + "default": "calculate_image_tiles", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["tiles"], + "title": "Calculate Image Tiles", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/CalculateImageTilesOutput" + } + }, + "CalculateImageTilesMinimumOverlapInvocation": { + "category": "tiles", + "class": "invocation", + "classification": "stable", + "description": "Calculate the coordinates and overlaps of tiles that cover a target image shape.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image_width": { + "default": 1024, + "description": "The image width, in pixels, to calculate tiles for.", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 1024, + "orig_required": false, + "title": "Image Width", + "type": "integer" + }, + "image_height": { + "default": 1024, + "description": "The image height, in pixels, to calculate tiles for.", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 1024, + "orig_required": false, + "title": "Image Height", + "type": "integer" + }, + "tile_width": { + "default": 576, + "description": "The tile width, in pixels.", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 576, + "orig_required": false, + "title": "Tile Width", + "type": "integer" + }, + "tile_height": { + "default": 576, + "description": "The tile height, in pixels.", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 576, + "orig_required": false, + "title": "Tile Height", + "type": "integer" + }, + "min_overlap": { + "default": 128, + "description": "Minimum overlap between adjacent tiles, in pixels.", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 128, + "orig_required": false, + "title": "Min Overlap", + "type": "integer" + }, + "type": { + "const": "calculate_image_tiles_min_overlap", + "default": "calculate_image_tiles_min_overlap", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["tiles"], + "title": "Calculate Image Tiles Minimum Overlap", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/CalculateImageTilesOutput" + } + }, + "CalculateImageTilesOutput": { + "class": "output", + "properties": { + "tiles": { + "description": "The tiles coordinates that cover a particular image shape.", + "field_kind": "output", + "items": { + "$ref": "#/components/schemas/Tile" + }, + "title": "Tiles", + "type": "array", + "ui_hidden": false + }, + "type": { + "const": "calculate_image_tiles_output", + "default": "calculate_image_tiles_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "tiles", "type", "type"], + "title": "CalculateImageTilesOutput", + "type": "object" + }, + "CancelAllExceptCurrentResult": { + "properties": { + "canceled": { + "type": "integer", + "title": "Canceled", + "description": "Number of queue items canceled" + } + }, + "type": "object", + "required": ["canceled"], + "title": "CancelAllExceptCurrentResult", + "description": "Result of canceling all except current" + }, + "CancelByBatchIDsResult": { + "properties": { + "canceled": { + "type": "integer", + "title": "Canceled", + "description": "Number of queue items canceled" + } + }, + "type": "object", + "required": ["canceled"], + "title": "CancelByBatchIDsResult", + "description": "Result of canceling by list of batch ids" + }, + "CancelByDestinationResult": { + "properties": { + "canceled": { + "type": "integer", + "title": "Canceled", + "description": "Number of queue items canceled" + } + }, + "type": "object", + "required": ["canceled"], + "title": "CancelByDestinationResult", + "description": "Result of canceling by a destination" + }, + "CannyEdgeDetectionInvocation": { + "category": "controlnet_preprocessors", + "class": "invocation", + "classification": "stable", + "description": "Geneartes an edge map using a cv2's Canny algorithm.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "low_threshold": { + "default": 100, + "description": "The low threshold of the Canny pixel gradient (0-255)", + "field_kind": "input", + "input": "any", + "maximum": 255, + "minimum": 0, + "orig_default": 100, + "orig_required": false, + "title": "Low Threshold", + "type": "integer" + }, + "high_threshold": { + "default": 200, + "description": "The high threshold of the Canny pixel gradient (0-255)", + "field_kind": "input", + "input": "any", + "maximum": 255, + "minimum": 0, + "orig_default": 200, + "orig_required": false, + "title": "High Threshold", + "type": "integer" + }, + "type": { + "const": "canny_edge_detection", + "default": "canny_edge_detection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["controlnet", "canny"], + "title": "Canny Edge Detection", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "CanvasOutputInvocation": { + "category": "canvas", + "class": "invocation", + "classification": "stable", + "description": "Outputs an image to the canvas staging area.\n\nUse this node in workflows intended for canvas workflow integration.\nConnect the final image of your workflow to this node to send it\nto the canvas staging area when run via 'Run Workflow on Canvas'.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": false, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "canvas_output", + "default": "canvas_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["canvas", "output", "image"], + "title": "Canvas Output", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "CanvasPasteBackInvocation": { + "category": "canvas", + "class": "invocation", + "classification": "stable", + "description": "Combines two images by using the mask provided. Intended for use on the Unified Canvas.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "source_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The source image", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "target_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The target image", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The mask to use when pasting", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "mask_blur": { + "default": 0, + "description": "The amount to blur the mask by", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Mask Blur", + "type": "integer" + }, + "type": { + "const": "canvas_paste_back", + "default": "canvas_paste_back", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "combine"], + "title": "Canvas Paste Back", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "CanvasV2MaskAndCropInvocation": { + "category": "canvas", + "class": "invocation", + "classification": "deprecated", + "description": "Handles Canvas V2 image output masking and cropping", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "source_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The source image onto which the masked generated image is pasted. If omitted, the masked generated image is returned with transparency.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "generated_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to apply the mask to", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The mask to apply", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "mask_blur": { + "default": 0, + "description": "The amount to blur the mask by", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Mask Blur", + "type": "integer" + }, + "type": { + "const": "canvas_v2_mask_and_crop", + "default": "canvas_v2_mask_and_crop", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "mask", "id"], + "title": "Canvas V2 Mask and Crop", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "CenterPadCropInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Pad or crop an image's sides from the center by specified pixels. Positive values are outside of the image.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to crop", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "left": { + "default": 0, + "description": "Number of pixels to pad/crop from the left (negative values crop inwards, positive values pad outwards)", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Left", + "type": "integer" + }, + "right": { + "default": 0, + "description": "Number of pixels to pad/crop from the right (negative values crop inwards, positive values pad outwards)", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Right", + "type": "integer" + }, + "top": { + "default": 0, + "description": "Number of pixels to pad/crop from the top (negative values crop inwards, positive values pad outwards)", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Top", + "type": "integer" + }, + "bottom": { + "default": 0, + "description": "Number of pixels to pad/crop from the bottom (negative values crop inwards, positive values pad outwards)", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Bottom", + "type": "integer" + }, + "type": { + "const": "img_pad_crop", + "default": "img_pad_crop", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "pad", "crop"], + "title": "Center Pad or Crop Image", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "Classification": { + "description": "The classification of an Invocation.\n- `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.\n- `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.\n- `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.\n- `Deprecated`: The invocation is deprecated and may be removed in a future version.\n- `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.\n- `Special`: The invocation is a special case and does not fit into any of the other classifications.", + "enum": ["stable", "beta", "prototype", "deprecated", "internal", "special"], + "title": "Classification", + "type": "string" + }, + "ClearResult": { + "properties": { + "deleted": { + "type": "integer", + "title": "Deleted", + "description": "Number of queue items deleted" + } + }, + "type": "object", + "required": ["deleted"], + "title": "ClearResult", + "description": "Result of clearing the session queue" + }, + "ClipVariantType": { + "type": "string", + "enum": ["large", "gigantic"], + "title": "ClipVariantType", + "description": "Variant type." + }, + "CogView4ConditioningField": { + "description": "A conditioning tensor primitive value", + "properties": { + "conditioning_name": { + "description": "The name of conditioning tensor", + "title": "Conditioning Name", + "type": "string" + } + }, + "required": ["conditioning_name"], + "title": "CogView4ConditioningField", + "type": "object" + }, + "CogView4ConditioningOutput": { + "class": "output", + "description": "Base class for nodes that output a CogView text conditioning tensor.", + "properties": { + "conditioning": { + "$ref": "#/components/schemas/CogView4ConditioningField", + "description": "Conditioning tensor", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "cogview4_conditioning_output", + "default": "cogview4_conditioning_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "conditioning", "type", "type"], + "title": "CogView4ConditioningOutput", + "type": "object" + }, + "CogView4DenoiseInvocation": { + "category": "latents", + "class": "invocation", + "classification": "prototype", + "description": "Run the denoising process with a CogView4 model.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "noise": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Noise tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoise_mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/DenoiseMaskField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "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.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoising_start": { + "default": 0.0, + "description": "When to start denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0.0, + "orig_required": false, + "title": "Denoising Start", + "type": "number" + }, + "denoising_end": { + "default": 1.0, + "description": "When to stop denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Denoising End", + "type": "number" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CogView4 model (Transformer) to load", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Transformer" + }, + "positive_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/CogView4ConditioningField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Positive conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "negative_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/CogView4ConditioningField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Negative conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "cfg_scale": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 3.5, + "description": "Classifier-Free Guidance scale", + "field_kind": "input", + "input": "any", + "orig_default": 3.5, + "orig_required": false, + "title": "CFG Scale" + }, + "width": { + "default": 1024, + "description": "Width of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 32, + "orig_default": 1024, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 1024, + "description": "Height of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 32, + "orig_default": 1024, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "steps": { + "default": 25, + "description": "Number of steps to run", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 25, + "orig_required": false, + "title": "Steps", + "type": "integer" + }, + "seed": { + "default": 0, + "description": "Randomness seed for reproducibility.", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Seed", + "type": "integer" + }, + "type": { + "const": "cogview4_denoise", + "default": "cogview4_denoise", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "cogview4"], + "title": "Denoise - CogView4", + "type": "object", + "version": "1.1.0", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "CogView4ImageToLatentsInvocation": { + "category": "latents", + "class": "invocation", + "classification": "prototype", + "description": "Generates latents from an image.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to encode.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "type": { + "const": "cogview4_i2l", + "default": "cogview4_i2l", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "latents", "vae", "i2l", "cogview4"], + "title": "Image to Latents - CogView4", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "CogView4LatentsToImageInvocation": { + "category": "latents", + "class": "invocation", + "classification": "prototype", + "description": "Generates an image from latents.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "type": { + "const": "cogview4_l2i", + "default": "cogview4_l2i", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "image", "vae", "l2i", "cogview4"], + "title": "Latents to Image - CogView4", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "CogView4ModelLoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "prototype", + "description": "Loads a CogView4 base model, outputting its submodels.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "CogView4 model (Transformer) to load", + "field_kind": "input", + "input": "direct", + "orig_required": true, + "ui_model_base": ["cogview4"], + "ui_model_type": ["main"] + }, + "type": { + "const": "cogview4_model_loader", + "default": "cogview4_model_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["model", "type", "id"], + "tags": ["model", "cogview4"], + "title": "Main Model - CogView4", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/CogView4ModelLoaderOutput" + } + }, + "CogView4ModelLoaderOutput": { + "class": "output", + "description": "CogView4 base model loader output.", + "properties": { + "transformer": { + "$ref": "#/components/schemas/TransformerField", + "description": "Transformer", + "field_kind": "output", + "title": "Transformer", + "ui_hidden": false + }, + "glm_encoder": { + "$ref": "#/components/schemas/GlmEncoderField", + "description": "GLM (THUDM) tokenizer and text encoder", + "field_kind": "output", + "title": "GLM Encoder", + "ui_hidden": false + }, + "vae": { + "$ref": "#/components/schemas/VAEField", + "description": "VAE", + "field_kind": "output", + "title": "VAE", + "ui_hidden": false + }, + "type": { + "const": "cogview4_model_loader_output", + "default": "cogview4_model_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "transformer", "glm_encoder", "vae", "type", "type"], + "title": "CogView4ModelLoaderOutput", + "type": "object" + }, + "CogView4TextEncoderInvocation": { + "category": "prompt", + "class": "invocation", + "classification": "prototype", + "description": "Encodes and preps a prompt for a cogview4 image.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Text prompt to encode.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Prompt", + "ui_component": "textarea" + }, + "glm_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/GlmEncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "GLM (THUDM) tokenizer and text encoder", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "GLM Encoder" + }, + "type": { + "const": "cogview4_text_encoder", + "default": "cogview4_text_encoder", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["prompt", "conditioning", "cogview4"], + "title": "Prompt - CogView4", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/CogView4ConditioningOutput" + } + }, + "CollectInvocation": { + "class": "invocation", + "classification": "stable", + "description": "Collects values into a collection", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "item": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "description": "The item to collect (all inputs must be of the same type)", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Collection Item", + "ui_type": "CollectionItemField" + }, + "collection": { + "default": [], + "description": "An optional collection to append to", + "field_kind": "input", + "input": "connection", + "items": {}, + "orig_default": [], + "orig_required": false, + "title": "Collection", + "type": "array", + "ui_type": "CollectionField" + }, + "type": { + "const": "collect", + "default": "collect", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "title": "CollectInvocation", + "type": "object", + "version": "1.1.0", + "output": { + "$ref": "#/components/schemas/CollectInvocationOutput" + } + }, + "CollectInvocationOutput": { + "class": "output", + "properties": { + "collection": { + "description": "The collection of input items", + "field_kind": "output", + "items": {}, + "title": "Collection", + "type": "array", + "ui_hidden": false, + "ui_type": "CollectionField" + }, + "type": { + "const": "collect_output", + "default": "collect_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "collection", "type", "type"], + "title": "CollectInvocationOutput", + "type": "object" + }, + "ColorCollectionOutput": { + "class": "output", + "description": "Base class for nodes that output a collection of colors", + "properties": { + "collection": { + "description": "The output colors", + "field_kind": "output", + "items": { + "$ref": "#/components/schemas/ColorField" + }, + "title": "Collection", + "type": "array", + "ui_hidden": false + }, + "type": { + "const": "color_collection_output", + "default": "color_collection_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "collection", "type", "type"], + "title": "ColorCollectionOutput", + "type": "object" + }, + "ColorCorrectInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Matches the color histogram of a base image to a reference image, optionally\nusing a mask to only color-correct certain regions of the base image.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "base_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to color-correct", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "color_reference": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Reference image for color-correction", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional mask to limit color correction area", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "colorspace": { + "default": "RGB", + "description": "Colorspace in which to apply histogram matching", + "enum": ["RGB", "YCbCr", "YCbCr-Chroma", "YCbCr-Luma"], + "field_kind": "input", + "input": "any", + "orig_default": "RGB", + "orig_required": false, + "title": "Color Space", + "type": "string" + }, + "type": { + "const": "color_correct", + "default": "color_correct", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "color"], + "title": "Color Correct", + "type": "object", + "version": "2.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ColorField": { + "description": "A color primitive field", + "properties": { + "r": { + "description": "The red component", + "maximum": 255, + "minimum": 0, + "title": "R", + "type": "integer" + }, + "g": { + "description": "The green component", + "maximum": 255, + "minimum": 0, + "title": "G", + "type": "integer" + }, + "b": { + "description": "The blue component", + "maximum": 255, + "minimum": 0, + "title": "B", + "type": "integer" + }, + "a": { + "description": "The alpha component", + "maximum": 255, + "minimum": 0, + "title": "A", + "type": "integer" + } + }, + "required": ["r", "g", "b", "a"], + "title": "ColorField", + "type": "object" + }, + "ColorInvocation": { + "category": "primitives", + "class": "invocation", + "classification": "stable", + "description": "A color primitive value", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "color": { + "$ref": "#/components/schemas/ColorField", + "default": { + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "description": "The color value", + "field_kind": "input", + "input": "any", + "orig_default": { + "a": 255, + "b": 0, + "g": 0, + "r": 0 + }, + "orig_required": false + }, + "type": { + "const": "color", + "default": "color", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "color"], + "title": "Color Primitive", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/ColorOutput" + } + }, + "ColorMapInvocation": { + "category": "controlnet_preprocessors", + "class": "invocation", + "classification": "stable", + "description": "Generates a color map from the provided image.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "tile_size": { + "default": 64, + "description": "Tile size", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 64, + "orig_required": false, + "title": "Tile Size", + "type": "integer" + }, + "type": { + "const": "color_map", + "default": "color_map", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["controlnet"], + "title": "Color Map", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ColorOutput": { + "class": "output", + "description": "Base class for nodes that output a single color", + "properties": { + "color": { + "$ref": "#/components/schemas/ColorField", + "description": "The output color", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "color_output", + "default": "color_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "color", "type", "type"], + "title": "ColorOutput", + "type": "object" + }, + "CompelInvocation": { + "category": "prompt", + "class": "invocation", + "classification": "stable", + "description": "Parse prompt using compel package to conditioning.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "prompt": { + "default": "", + "description": "Prompt to be parsed by Compel to create a conditioning tensor", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Prompt", + "type": "string", + "ui_component": "textarea" + }, + "clip": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "CLIP" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A mask defining the region that this conditioning prompt applies to.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "type": { + "const": "compel", + "default": "compel", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["prompt", "compel"], + "title": "Prompt - SD1.5", + "type": "object", + "version": "1.2.1", + "output": { + "$ref": "#/components/schemas/ConditioningOutput" + } + }, + "ConditioningCollectionInvocation": { + "category": "primitives", + "class": "invocation", + "classification": "stable", + "description": "A collection of conditioning tensor primitive values", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "collection": { + "default": [], + "description": "The collection of conditioning tensors", + "field_kind": "input", + "input": "any", + "items": { + "$ref": "#/components/schemas/ConditioningField" + }, + "orig_default": [], + "orig_required": false, + "title": "Collection", + "type": "array" + }, + "type": { + "const": "conditioning_collection", + "default": "conditioning_collection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "conditioning", "collection"], + "title": "Conditioning Collection Primitive", + "type": "object", + "version": "1.0.2", + "output": { + "$ref": "#/components/schemas/ConditioningCollectionOutput" + } + }, + "ConditioningCollectionOutput": { + "class": "output", + "description": "Base class for nodes that output a collection of conditioning tensors", + "properties": { + "collection": { + "description": "The output conditioning tensors", + "field_kind": "output", + "items": { + "$ref": "#/components/schemas/ConditioningField" + }, + "title": "Collection", + "type": "array", + "ui_hidden": false + }, + "type": { + "const": "conditioning_collection_output", + "default": "conditioning_collection_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "collection", "type", "type"], + "title": "ConditioningCollectionOutput", + "type": "object" + }, + "ConditioningField": { + "description": "A conditioning tensor primitive value", + "properties": { + "conditioning_name": { + "description": "The name of conditioning tensor", + "title": "Conditioning Name", + "type": "string" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The mask associated with this conditioning tensor. Excluded regions should be set to False, included regions should be set to True." + } + }, + "required": ["conditioning_name"], + "title": "ConditioningField", + "type": "object" + }, + "ConditioningInvocation": { + "category": "primitives", + "class": "invocation", + "classification": "stable", + "description": "A conditioning tensor primitive value", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/ConditioningField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "type": { + "const": "conditioning", + "default": "conditioning", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "conditioning"], + "title": "Conditioning Primitive", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/ConditioningOutput" + } + }, + "ConditioningOutput": { + "class": "output", + "description": "Base class for nodes that output a single conditioning tensor", + "properties": { + "conditioning": { + "$ref": "#/components/schemas/ConditioningField", + "description": "Conditioning tensor", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "conditioning_output", + "default": "conditioning_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "conditioning", "type", "type"], + "title": "ConditioningOutput", + "type": "object" + }, + "ContentShuffleInvocation": { + "category": "controlnet_preprocessors", + "class": "invocation", + "classification": "stable", + "description": "Shuffles the image, similar to a 'liquify' filter.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "scale_factor": { + "default": 256, + "description": "The scale factor used for the shuffle", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 256, + "orig_required": false, + "title": "Scale Factor", + "type": "integer" + }, + "type": { + "const": "content_shuffle", + "default": "content_shuffle", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["controlnet", "normal"], + "title": "Content Shuffle", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ControlAdapterDefaultSettings": { + "properties": { + "preprocessor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Preprocessor" + }, + "fp8_storage": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Fp8 Storage", + "description": "Store weights in FP8 to reduce VRAM usage (~50% savings). Weights are cast to compute dtype during inference." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["preprocessor"], + "title": "ControlAdapterDefaultSettings" + }, + "ControlField": { + "properties": { + "image": { + "$ref": "#/components/schemas/ImageField", + "description": "The control image" + }, + "control_model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "The ControlNet model to use" + }, + "control_weight": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 1, + "description": "The weight given to the ControlNet", + "title": "Control Weight" + }, + "begin_step_percent": { + "default": 0, + "description": "When the ControlNet is first applied (% of total steps)", + "maximum": 1, + "minimum": 0, + "title": "Begin Step Percent", + "type": "number" + }, + "end_step_percent": { + "default": 1, + "description": "When the ControlNet is last applied (% of total steps)", + "maximum": 1, + "minimum": 0, + "title": "End Step Percent", + "type": "number" + }, + "control_mode": { + "default": "balanced", + "description": "The control mode to use", + "enum": ["balanced", "more_prompt", "more_control", "unbalanced"], + "title": "Control Mode", + "type": "string" + }, + "resize_mode": { + "default": "just_resize", + "description": "The resize mode to use", + "enum": ["just_resize", "crop_resize", "fill_resize", "just_resize_simple"], + "title": "Resize Mode", + "type": "string" + } + }, + "required": ["image", "control_model"], + "title": "ControlField", + "type": "object" + }, + "ControlLoRAField": { + "properties": { + "lora": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Info to load lora model" + }, + "weight": { + "description": "Weight to apply to lora model", + "title": "Weight", + "type": "number" + }, + "img": { + "$ref": "#/components/schemas/ImageField", + "description": "Image to use in structural conditioning" + } + }, + "required": ["lora", "weight", "img"], + "title": "ControlLoRAField", + "type": "object" + }, + "ControlLoRA_LyCORIS_FLUX_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlAdapterDefaultSettings" + }, + { + "type": "null" + } + ] + }, + "base": { + "type": "string", + "const": "flux", + "title": "Base", + "default": "flux" + }, + "type": { + "type": "string", + "const": "control_lora", + "title": "Type", + "default": "control_lora" + }, + "format": { + "type": "string", + "const": "lycoris", + "title": "Format", + "default": "lycoris" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "default_settings", + "base", + "type", + "format", + "trigger_phrases" + ], + "title": "ControlLoRA_LyCORIS_FLUX_Config", + "description": "Model config for Control LoRA models." + }, + "ControlNetInvocation": { + "category": "conditioning", + "class": "invocation", + "classification": "stable", + "description": "Collects ControlNet info to pass to other nodes", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The control image", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "control_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "ControlNet model to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "ui_model_base": ["sd-1", "sd-2", "sdxl"], + "ui_model_type": ["controlnet"] + }, + "control_weight": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 1.0, + "description": "The weight given to the ControlNet", + "field_kind": "input", + "ge": -1, + "input": "any", + "le": 2, + "orig_default": 1.0, + "orig_required": false, + "title": "Control Weight" + }, + "begin_step_percent": { + "default": 0, + "description": "When the ControlNet is first applied (% of total steps)", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Begin Step Percent", + "type": "number" + }, + "end_step_percent": { + "default": 1, + "description": "When the ControlNet is last applied (% of total steps)", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1, + "orig_required": false, + "title": "End Step Percent", + "type": "number" + }, + "control_mode": { + "default": "balanced", + "description": "The control mode used", + "enum": ["balanced", "more_prompt", "more_control", "unbalanced"], + "field_kind": "input", + "input": "any", + "orig_default": "balanced", + "orig_required": false, + "title": "Control Mode", + "type": "string" + }, + "resize_mode": { + "default": "just_resize", + "description": "The resize mode used", + "enum": ["just_resize", "crop_resize", "fill_resize", "just_resize_simple"], + "field_kind": "input", + "input": "any", + "orig_default": "just_resize", + "orig_required": false, + "title": "Resize Mode", + "type": "string" + }, + "type": { + "const": "controlnet", + "default": "controlnet", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["controlnet"], + "title": "ControlNet - SD1.5, SD2, SDXL", + "type": "object", + "version": "1.1.3", + "output": { + "$ref": "#/components/schemas/ControlOutput" + } + }, + "ControlNetMetadataField": { + "properties": { + "image": { + "$ref": "#/components/schemas/ImageField", + "description": "The control image" + }, + "processed_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The control image, after processing." + }, + "control_model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "The ControlNet model to use" + }, + "control_weight": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 1, + "description": "The weight given to the ControlNet", + "title": "Control Weight" + }, + "begin_step_percent": { + "default": 0, + "description": "When the ControlNet is first applied (% of total steps)", + "maximum": 1, + "minimum": 0, + "title": "Begin Step Percent", + "type": "number" + }, + "end_step_percent": { + "default": 1, + "description": "When the ControlNet is last applied (% of total steps)", + "maximum": 1, + "minimum": 0, + "title": "End Step Percent", + "type": "number" + }, + "control_mode": { + "default": "balanced", + "description": "The control mode to use", + "enum": ["balanced", "more_prompt", "more_control", "unbalanced"], + "title": "Control Mode", + "type": "string" + }, + "resize_mode": { + "default": "just_resize", + "description": "The resize mode to use", + "enum": ["just_resize", "crop_resize", "fill_resize", "just_resize_simple"], + "title": "Resize Mode", + "type": "string" + } + }, + "required": ["image", "control_model"], + "title": "ControlNetMetadataField", + "type": "object" + }, + "ControlNetRecallParameter": { + "properties": { + "model_name": { + "type": "string", + "title": "Model Name", + "description": "The name of the ControlNet/T2I Adapter/Control LoRA model" + }, + "image_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Image Name", + "description": "The filename of the control image in outputs/images" + }, + "weight": { + "type": "number", + "maximum": 2.0, + "minimum": -1.0, + "title": "Weight", + "description": "The weight for the control adapter", + "default": 1.0 + }, + "begin_step_percent": { + "anyOf": [ + { + "type": "number", + "maximum": 1.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Begin Step Percent", + "description": "When the control adapter is first applied (% of total steps)" + }, + "end_step_percent": { + "anyOf": [ + { + "type": "number", + "maximum": 1.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "End Step Percent", + "description": "When the control adapter is last applied (% of total steps)" + }, + "control_mode": { + "anyOf": [ + { + "type": "string", + "enum": ["balanced", "more_prompt", "more_control"] + }, + { + "type": "null" + } + ], + "title": "Control Mode", + "description": "The control mode (ControlNet only)" + } + }, + "type": "object", + "required": ["model_name"], + "title": "ControlNetRecallParameter", + "description": "ControlNet configuration for recall" + }, + "ControlNet_Checkpoint_FLUX_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "type": { + "type": "string", + "const": "controlnet", + "title": "Type", + "default": "controlnet" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlAdapterDefaultSettings" + }, + { + "type": "null" + } + ] + }, + "base": { + "type": "string", + "const": "flux", + "title": "Base", + "default": "flux" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "config_path", + "type", + "format", + "default_settings", + "base" + ], + "title": "ControlNet_Checkpoint_FLUX_Config" + }, + "ControlNet_Checkpoint_SD1_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "type": { + "type": "string", + "const": "controlnet", + "title": "Type", + "default": "controlnet" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlAdapterDefaultSettings" + }, + { + "type": "null" + } + ] + }, + "base": { + "type": "string", + "const": "sd-1", + "title": "Base", + "default": "sd-1" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "config_path", + "type", + "format", + "default_settings", + "base" + ], + "title": "ControlNet_Checkpoint_SD1_Config" + }, + "ControlNet_Checkpoint_SD2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "type": { + "type": "string", + "const": "controlnet", + "title": "Type", + "default": "controlnet" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlAdapterDefaultSettings" + }, + { + "type": "null" + } + ] + }, + "base": { + "type": "string", + "const": "sd-2", + "title": "Base", + "default": "sd-2" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "config_path", + "type", + "format", + "default_settings", + "base" + ], + "title": "ControlNet_Checkpoint_SD2_Config" + }, + "ControlNet_Checkpoint_SDXL_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "type": { + "type": "string", + "const": "controlnet", + "title": "Type", + "default": "controlnet" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlAdapterDefaultSettings" + }, + { + "type": "null" + } + ] + }, + "base": { + "type": "string", + "const": "sdxl", + "title": "Base", + "default": "sdxl" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "config_path", + "type", + "format", + "default_settings", + "base" + ], + "title": "ControlNet_Checkpoint_SDXL_Config" + }, + "ControlNet_Checkpoint_ZImage_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "type": { + "type": "string", + "const": "controlnet", + "title": "Type", + "default": "controlnet" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "base": { + "type": "string", + "const": "z-image", + "title": "Base", + "default": "z-image" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlAdapterDefaultSettings" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "config_path", + "type", + "format", + "base", + "default_settings" + ], + "title": "ControlNet_Checkpoint_ZImage_Config", + "description": "Model config for Z-Image Control adapter models (Safetensors checkpoint).\n\nZ-Image Control models are standalone adapters containing only the control layers\n(control_layers, control_all_x_embedder, control_noise_refiner) that extend\nthe base Z-Image transformer with spatial conditioning capabilities.\n\nSupports: Canny, HED, Depth, Pose, MLSD.\nRecommended control_context_scale: 0.65-0.80." + }, + "ControlNet_Diffusers_FLUX_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "type": { + "type": "string", + "const": "controlnet", + "title": "Type", + "default": "controlnet" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlAdapterDefaultSettings" + }, + { + "type": "null" + } + ] + }, + "base": { + "type": "string", + "const": "flux", + "title": "Base", + "default": "flux" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "format", + "repo_variant", + "type", + "default_settings", + "base" + ], + "title": "ControlNet_Diffusers_FLUX_Config" + }, + "ControlNet_Diffusers_SD1_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "type": { + "type": "string", + "const": "controlnet", + "title": "Type", + "default": "controlnet" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlAdapterDefaultSettings" + }, + { + "type": "null" + } + ] + }, + "base": { + "type": "string", + "const": "sd-1", + "title": "Base", + "default": "sd-1" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "format", + "repo_variant", + "type", + "default_settings", + "base" + ], + "title": "ControlNet_Diffusers_SD1_Config" + }, + "ControlNet_Diffusers_SD2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "type": { + "type": "string", + "const": "controlnet", + "title": "Type", + "default": "controlnet" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlAdapterDefaultSettings" + }, + { + "type": "null" + } + ] + }, + "base": { + "type": "string", + "const": "sd-2", + "title": "Base", + "default": "sd-2" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "format", + "repo_variant", + "type", + "default_settings", + "base" + ], + "title": "ControlNet_Diffusers_SD2_Config" + }, + "ControlNet_Diffusers_SDXL_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "type": { + "type": "string", + "const": "controlnet", + "title": "Type", + "default": "controlnet" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlAdapterDefaultSettings" + }, + { + "type": "null" + } + ] + }, + "base": { + "type": "string", + "const": "sdxl", + "title": "Base", + "default": "sdxl" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "format", + "repo_variant", + "type", + "default_settings", + "base" + ], + "title": "ControlNet_Diffusers_SDXL_Config" + }, + "ControlOutput": { + "class": "output", + "description": "node output for ControlNet info", + "properties": { + "control": { + "$ref": "#/components/schemas/ControlField", + "description": "ControlNet(s) to apply", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "control_output", + "default": "control_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "control", "type", "type"], + "title": "ControlOutput", + "type": "object" + }, + "CoreMetadataInvocation": { + "additionalProperties": true, + "category": "metadata", + "class": "invocation", + "classification": "internal", + "description": "Used internally by Invoke to collect metadata for generations.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "generation_mode": { + "anyOf": [ + { + "enum": [ + "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" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The generation mode that output this image", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Generation Mode" + }, + "positive_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The positive prompt parameter", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Positive Prompt" + }, + "negative_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The negative prompt parameter", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Negative Prompt" + }, + "width": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The width parameter", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Width" + }, + "height": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The height parameter", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Height" + }, + "seed": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The seed used for noise generation", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Seed" + }, + "rand_device": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The device used for random number generation", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Rand Device" + }, + "cfg_scale": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The classifier-free guidance scale parameter", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Cfg Scale" + }, + "cfg_rescale_multiplier": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Rescale multiplier for CFG guidance, used for models trained with zero-terminal SNR", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Cfg Rescale Multiplier" + }, + "steps": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The number of steps used for inference", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Steps" + }, + "scheduler": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The scheduler used for inference", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Scheduler" + }, + "seamless_x": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Whether seamless tiling was used on the X axis", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Seamless X" + }, + "seamless_y": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Whether seamless tiling was used on the Y axis", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Seamless Y" + }, + "clip_skip": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The number of skipped CLIP layers", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Clip Skip" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The main model used for inference", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "controlnets": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ControlNetMetadataField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The ControlNets used for inference", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Controlnets" + }, + "ipAdapters": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/IPAdapterMetadataField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The IP Adapters used for inference", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Ipadapters" + }, + "t2iAdapters": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/T2IAdapterMetadataField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The IP Adapters used for inference", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "T2Iadapters" + }, + "loras": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/LoRAMetadataField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The LoRAs used for inference", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Loras" + }, + "strength": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The strength used for latents-to-latents", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Strength" + }, + "init_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The name of the initial image", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Init Image" + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The VAE used for decoding, if the main model's default was not used", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "qwen3_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The Qwen3 text encoder model used for Z-Image inference", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "hrf_enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Whether or not high resolution fix was enabled.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Hrf Enabled" + }, + "hrf_method": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The high resolution fix upscale method.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Hrf Method" + }, + "hrf_strength": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The high resolution fix img2img strength used in the upscale pass.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Hrf Strength" + }, + "positive_style_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The positive style prompt parameter", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Positive Style Prompt" + }, + "negative_style_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The negative style prompt parameter", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Negative Style Prompt" + }, + "refiner_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The SDXL Refiner model used", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "refiner_cfg_scale": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The classifier-free guidance scale parameter used for the refiner", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Refiner Cfg Scale" + }, + "refiner_steps": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The number of steps used for the refiner", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Refiner Steps" + }, + "refiner_scheduler": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The scheduler used for the refiner", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Refiner Scheduler" + }, + "refiner_positive_aesthetic_score": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The aesthetic score used for the refiner", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Refiner Positive Aesthetic Score" + }, + "refiner_negative_aesthetic_score": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The aesthetic score used for the refiner", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Refiner Negative Aesthetic Score" + }, + "refiner_start": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The start value used for refiner denoising", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Refiner Start" + }, + "type": { + "const": "core_metadata", + "default": "core_metadata", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Core Metadata", + "type": "object", + "version": "2.1.0", + "output": { + "$ref": "#/components/schemas/MetadataOutput" + } + }, + "CreateDenoiseMaskInvocation": { + "category": "mask", + "class": "invocation", + "classification": "stable", + "description": "Creates mask for denoising model run.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "ui_order": 0 + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Image which will be masked", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "ui_order": 1 + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The mask to use when pasting", + "field_kind": "input", + "input": "any", + "orig_required": true, + "ui_order": 2 + }, + "tiled": { + "default": false, + "description": "Processing using overlapping tiles (reduce memory consumption)", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Tiled", + "type": "boolean", + "ui_order": 3 + }, + "fp32": { + "default": false, + "description": "Whether or not to use full float32 precision", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Fp32", + "type": "boolean", + "ui_order": 4 + }, + "type": { + "const": "create_denoise_mask", + "default": "create_denoise_mask", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["mask", "denoise"], + "title": "Create Denoise Mask", + "type": "object", + "version": "1.0.2", + "output": { + "$ref": "#/components/schemas/DenoiseMaskOutput" + } + }, + "CreateGradientMaskInvocation": { + "category": "mask", + "class": "invocation", + "classification": "stable", + "description": "Creates mask for denoising.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Image which will be masked", + "field_kind": "input", + "input": "any", + "orig_required": true, + "ui_order": 1 + }, + "edge_radius": { + "default": 16, + "description": "How far to expand the edges of the mask", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 16, + "orig_required": false, + "title": "Edge Radius", + "type": "integer", + "ui_order": 2 + }, + "coherence_mode": { + "default": "Gaussian Blur", + "enum": ["Gaussian Blur", "Box Blur", "Staged"], + "field_kind": "input", + "input": "any", + "orig_default": "Gaussian Blur", + "orig_required": false, + "title": "Coherence Mode", + "type": "string", + "ui_order": 3 + }, + "minimum_denoise": { + "default": 0.0, + "description": "Minimum denoise level for the coherence region", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0.0, + "orig_required": false, + "title": "Minimum Denoise", + "type": "number", + "ui_order": 4 + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "OPTIONAL: Only connect for specialized Inpainting models, masked_latents will be generated from the image with the VAE", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "[OPTIONAL] Image", + "ui_order": 6 + }, + "unet": { + "anyOf": [ + { + "$ref": "#/components/schemas/UNetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "OPTIONAL: If the Unet is a specialized Inpainting model, masked_latents will be generated from the image with the VAE", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "[OPTIONAL] UNet", + "ui_order": 5 + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "OPTIONAL: Only connect for specialized Inpainting models, masked_latents will be generated from the image with the VAE", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "[OPTIONAL] VAE", + "ui_order": 7 + }, + "tiled": { + "default": false, + "description": "Processing using overlapping tiles (reduce memory consumption)", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Tiled", + "type": "boolean", + "ui_order": 8 + }, + "fp32": { + "default": false, + "description": "Whether or not to use full float32 precision", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Fp32", + "type": "boolean", + "ui_order": 9 + }, + "type": { + "const": "create_gradient_mask", + "default": "create_gradient_mask", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["mask", "denoise"], + "title": "Create Gradient Mask", + "type": "object", + "version": "1.3.0", + "output": { + "$ref": "#/components/schemas/GradientMaskOutput" + } + }, + "CropImageToBoundingBoxInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Crop an image to the given bounding box. If the bounding box is omitted, the image is cropped to the non-transparent pixels.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to crop", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "bounding_box": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoundingBoxField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The bounding box to crop the image to", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "type": { + "const": "crop_image_to_bounding_box", + "default": "crop_image_to_bounding_box", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "crop"], + "title": "Crop Image to Bounding Box", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "CropLatentsCoreInvocation": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Crops a latent-space tensor to a box specified in image-space. The box dimensions and coordinates must be\ndivisible by the latent scale factor of 8.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "x": { + "anyOf": [ + { + "minimum": 0, + "multipleOf": 8, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "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.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "X" + }, + "y": { + "anyOf": [ + { + "minimum": 0, + "multipleOf": 8, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "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.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Y" + }, + "width": { + "anyOf": [ + { + "minimum": 1, + "multipleOf": 8, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The width (in px) of the crop rectangle in image space. This value will be converted to a dimension in latent space.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Width" + }, + "height": { + "anyOf": [ + { + "minimum": 1, + "multipleOf": 8, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The height (in px) of the crop rectangle in image space. This value will be converted to a dimension in latent space.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Height" + }, + "type": { + "const": "crop_latents", + "default": "crop_latents", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "crop"], + "title": "Crop Latents", + "type": "object", + "version": "1.0.2", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "CvInpaintInvocation": { + "category": "inpaint", + "class": "invocation", + "classification": "stable", + "description": "Simple inpaint using opencv.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to inpaint", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The mask to use when inpainting", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "cv_inpaint", + "default": "cv_inpaint", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["opencv", "inpaint"], + "title": "OpenCV Inpaint", + "type": "object", + "version": "1.3.1", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "DWOpenposeDetectionInvocation": { + "category": "controlnet_preprocessors", + "class": "invocation", + "classification": "stable", + "description": "Generates an openpose pose from an image using DWPose", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "draw_body": { + "default": true, + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "Draw Body", + "type": "boolean" + }, + "draw_face": { + "default": false, + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Draw Face", + "type": "boolean" + }, + "draw_hands": { + "default": false, + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Draw Hands", + "type": "boolean" + }, + "type": { + "const": "dw_openpose_detection", + "default": "dw_openpose_detection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["controlnet", "dwpose", "openpose"], + "title": "DW Openpose Detection", + "type": "object", + "version": "1.1.1", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "DecodeInvisibleWatermarkInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Decode an invisible watermark from an image.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to decode the watermark from", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "length": { + "default": 8, + "description": "The expected watermark length in bytes", + "field_kind": "input", + "input": "any", + "orig_default": 8, + "orig_required": false, + "title": "Length", + "type": "integer" + }, + "type": { + "const": "decode_watermark", + "default": "decode_watermark", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "watermark"], + "title": "Decode Invisible Watermark", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/StringOutput" + } + }, + "DeleteAllExceptCurrentResult": { + "properties": { + "deleted": { + "type": "integer", + "title": "Deleted", + "description": "Number of queue items deleted" + } + }, + "type": "object", + "required": ["deleted"], + "title": "DeleteAllExceptCurrentResult", + "description": "Result of deleting all except current" + }, + "DeleteBoardResult": { + "properties": { + "board_id": { + "type": "string", + "title": "Board Id", + "description": "The id of the board that was deleted." + }, + "deleted_board_images": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Deleted Board Images", + "description": "The image names of the board-images relationships that were deleted." + }, + "deleted_images": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Deleted Images", + "description": "The names of the images that were deleted." + } + }, + "type": "object", + "required": ["board_id", "deleted_board_images", "deleted_images"], + "title": "DeleteBoardResult" + }, + "DeleteByDestinationResult": { + "properties": { + "deleted": { + "type": "integer", + "title": "Deleted", + "description": "Number of queue items deleted" + } + }, + "type": "object", + "required": ["deleted"], + "title": "DeleteByDestinationResult", + "description": "Result of deleting by a destination" + }, + "DeleteImagesResult": { + "properties": { + "affected_boards": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Affected Boards", + "description": "The ids of boards affected by the delete operation" + }, + "deleted_images": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Deleted Images", + "description": "The names of the images that were deleted" + } + }, + "type": "object", + "required": ["affected_boards", "deleted_images"], + "title": "DeleteImagesResult" + }, + "DeleteOrphanedModelsRequest": { + "properties": { + "paths": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Paths", + "description": "List of relative paths to delete" + } + }, + "type": "object", + "required": ["paths"], + "title": "DeleteOrphanedModelsRequest", + "description": "Request to delete specific orphaned model directories." + }, + "DeleteOrphanedModelsResponse": { + "properties": { + "deleted": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Deleted", + "description": "Paths that were successfully deleted" + }, + "errors": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Errors", + "description": "Paths that had errors, with error messages" + } + }, + "type": "object", + "required": ["deleted", "errors"], + "title": "DeleteOrphanedModelsResponse", + "description": "Response from deleting orphaned models." + }, + "DenoiseLatentsInvocation": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Denoises noisy latents to decodable images", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "positive_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/ConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/ConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Positive conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Positive Conditioning", + "ui_order": 0 + }, + "negative_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/ConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/ConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Negative conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Negative Conditioning", + "ui_order": 1 + }, + "noise": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Noise tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "ui_order": 3 + }, + "steps": { + "default": 10, + "description": "Number of steps to run", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 10, + "orig_required": false, + "title": "Steps", + "type": "integer" + }, + "cfg_scale": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 7.5, + "description": "Classifier-Free Guidance scale", + "field_kind": "input", + "input": "any", + "orig_default": 7.5, + "orig_required": false, + "title": "CFG Scale" + }, + "denoising_start": { + "default": 0.0, + "description": "When to start denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0.0, + "orig_required": false, + "title": "Denoising Start", + "type": "number" + }, + "denoising_end": { + "default": 1.0, + "description": "When to stop denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Denoising End", + "type": "number" + }, + "scheduler": { + "default": "euler", + "description": "Scheduler to use during inference", + "enum": [ + "ddim", + "ddpm", + "deis", + "deis_k", + "lms", + "lms_k", + "pndm", + "heun", + "heun_k", + "euler", + "euler_k", + "euler_a", + "kdpm_2", + "kdpm_2_k", + "kdpm_2_a", + "kdpm_2_a_k", + "dpmpp_2s", + "dpmpp_2s_k", + "dpmpp_2m", + "dpmpp_2m_k", + "dpmpp_2m_sde", + "dpmpp_2m_sde_k", + "dpmpp_3m", + "dpmpp_3m_k", + "dpmpp_sde", + "dpmpp_sde_k", + "er_sde", + "unipc", + "unipc_k", + "lcm", + "tcd" + ], + "field_kind": "input", + "input": "any", + "orig_default": "euler", + "orig_required": false, + "title": "Scheduler", + "type": "string", + "ui_type": "SchedulerField" + }, + "unet": { + "anyOf": [ + { + "$ref": "#/components/schemas/UNetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "UNet (scheduler, LoRAs)", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "UNet", + "ui_order": 2 + }, + "control": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlField" + }, + { + "items": { + "$ref": "#/components/schemas/ControlField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Control", + "ui_order": 5 + }, + "ip_adapter": { + "anyOf": [ + { + "$ref": "#/components/schemas/IPAdapterField" + }, + { + "items": { + "$ref": "#/components/schemas/IPAdapterField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "IP-Adapter to apply", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "IP-Adapter", + "ui_order": 6 + }, + "t2i_adapter": { + "anyOf": [ + { + "$ref": "#/components/schemas/T2IAdapterField" + }, + { + "items": { + "$ref": "#/components/schemas/T2IAdapterField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "T2I-Adapter(s) to apply", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "T2I-Adapter", + "ui_order": 7 + }, + "cfg_rescale_multiplier": { + "default": 0, + "description": "Rescale multiplier for CFG guidance, used for models trained with zero-terminal SNR", + "exclusiveMaximum": 1, + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "CFG Rescale Multiplier", + "type": "number" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "ui_order": 4 + }, + "denoise_mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/DenoiseMaskField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "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.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "ui_order": 8 + }, + "type": { + "const": "denoise_latents", + "default": "denoise_latents", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"], + "title": "Denoise - SD1.5, SDXL", + "type": "object", + "version": "1.5.4", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "DenoiseLatentsMetaInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "stable", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "positive_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/ConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/ConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Positive conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Positive Conditioning", + "ui_order": 0 + }, + "negative_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/ConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/ConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Negative conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Negative Conditioning", + "ui_order": 1 + }, + "noise": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Noise tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "ui_order": 3 + }, + "steps": { + "default": 10, + "description": "Number of steps to run", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 10, + "orig_required": false, + "title": "Steps", + "type": "integer" + }, + "cfg_scale": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 7.5, + "description": "Classifier-Free Guidance scale", + "field_kind": "input", + "input": "any", + "orig_default": 7.5, + "orig_required": false, + "title": "CFG Scale" + }, + "denoising_start": { + "default": 0.0, + "description": "When to start denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0.0, + "orig_required": false, + "title": "Denoising Start", + "type": "number" + }, + "denoising_end": { + "default": 1.0, + "description": "When to stop denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Denoising End", + "type": "number" + }, + "scheduler": { + "default": "euler", + "description": "Scheduler to use during inference", + "enum": [ + "ddim", + "ddpm", + "deis", + "deis_k", + "lms", + "lms_k", + "pndm", + "heun", + "heun_k", + "euler", + "euler_k", + "euler_a", + "kdpm_2", + "kdpm_2_k", + "kdpm_2_a", + "kdpm_2_a_k", + "dpmpp_2s", + "dpmpp_2s_k", + "dpmpp_2m", + "dpmpp_2m_k", + "dpmpp_2m_sde", + "dpmpp_2m_sde_k", + "dpmpp_3m", + "dpmpp_3m_k", + "dpmpp_sde", + "dpmpp_sde_k", + "er_sde", + "unipc", + "unipc_k", + "lcm", + "tcd" + ], + "field_kind": "input", + "input": "any", + "orig_default": "euler", + "orig_required": false, + "title": "Scheduler", + "type": "string", + "ui_type": "SchedulerField" + }, + "unet": { + "anyOf": [ + { + "$ref": "#/components/schemas/UNetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "UNet (scheduler, LoRAs)", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "UNet", + "ui_order": 2 + }, + "control": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlField" + }, + { + "items": { + "$ref": "#/components/schemas/ControlField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Control", + "ui_order": 5 + }, + "ip_adapter": { + "anyOf": [ + { + "$ref": "#/components/schemas/IPAdapterField" + }, + { + "items": { + "$ref": "#/components/schemas/IPAdapterField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "IP-Adapter to apply", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "IP-Adapter", + "ui_order": 6 + }, + "t2i_adapter": { + "anyOf": [ + { + "$ref": "#/components/schemas/T2IAdapterField" + }, + { + "items": { + "$ref": "#/components/schemas/T2IAdapterField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "T2I-Adapter(s) to apply", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "T2I-Adapter", + "ui_order": 7 + }, + "cfg_rescale_multiplier": { + "default": 0, + "description": "Rescale multiplier for CFG guidance, used for models trained with zero-terminal SNR", + "exclusiveMaximum": 1, + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "CFG Rescale Multiplier", + "type": "number" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "ui_order": 4 + }, + "denoise_mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/DenoiseMaskField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "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.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "ui_order": 8 + }, + "type": { + "const": "denoise_latents_meta", + "default": "denoise_latents_meta", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"], + "title": "Denoise - SD1.5, SDXL + Metadata", + "type": "object", + "version": "1.1.1", + "output": { + "$ref": "#/components/schemas/LatentsMetaOutput" + } + }, + "DenoiseMaskField": { + "description": "An inpaint mask field", + "properties": { + "mask_name": { + "description": "The name of the mask image", + "title": "Mask Name", + "type": "string" + }, + "masked_latents_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The name of the masked image latents", + "title": "Masked Latents Name" + }, + "gradient": { + "default": false, + "description": "Used for gradient inpainting", + "title": "Gradient", + "type": "boolean" + } + }, + "required": ["mask_name"], + "title": "DenoiseMaskField", + "type": "object" + }, + "DenoiseMaskOutput": { + "class": "output", + "description": "Base class for nodes that output a single image", + "properties": { + "denoise_mask": { + "$ref": "#/components/schemas/DenoiseMaskField", + "description": "Mask for denoise model run", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "denoise_mask_output", + "default": "denoise_mask_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "denoise_mask", "type", "type"], + "title": "DenoiseMaskOutput", + "type": "object" + }, + "DepthAnythingDepthEstimationInvocation": { + "category": "controlnet_preprocessors", + "class": "invocation", + "classification": "stable", + "description": "Generates a depth map using a Depth Anything model.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "model_size": { + "default": "small_v2", + "description": "The size of the depth model to use", + "enum": ["large", "base", "small", "small_v2"], + "field_kind": "input", + "input": "any", + "orig_default": "small_v2", + "orig_required": false, + "title": "Model Size", + "type": "string" + }, + "type": { + "const": "depth_anything_depth_estimation", + "default": "depth_anything_depth_estimation", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["controlnet", "depth", "depth anything"], + "title": "Depth Anything Depth Estimation", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "DivideInvocation": { + "category": "math", + "class": "invocation", + "classification": "stable", + "description": "Divides two numbers", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "a": { + "default": 0, + "description": "The first number", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "A", + "type": "integer" + }, + "b": { + "default": 0, + "description": "The second number", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "B", + "type": "integer" + }, + "type": { + "const": "div", + "default": "div", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["math", "divide"], + "title": "Divide Integers", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/IntegerOutput" + } + }, + "DownloadCancelledEvent": { + "description": "Event model for download_cancelled", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "source": { + "description": "The source of the download", + "title": "Source", + "type": "string" + } + }, + "required": ["timestamp", "source"], + "title": "DownloadCancelledEvent", + "type": "object" + }, + "DownloadCompleteEvent": { + "description": "Event model for download_complete", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "source": { + "description": "The source of the download", + "title": "Source", + "type": "string" + }, + "download_path": { + "description": "The local path where the download is saved", + "title": "Download Path", + "type": "string" + }, + "total_bytes": { + "description": "The total number of bytes downloaded", + "title": "Total Bytes", + "type": "integer" + } + }, + "required": ["timestamp", "source", "download_path", "total_bytes"], + "title": "DownloadCompleteEvent", + "type": "object" + }, + "DownloadErrorEvent": { + "description": "Event model for download_error", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "source": { + "description": "The source of the download", + "title": "Source", + "type": "string" + }, + "error_type": { + "description": "The type of error", + "title": "Error Type", + "type": "string" + }, + "error": { + "description": "The error message", + "title": "Error", + "type": "string" + } + }, + "required": ["timestamp", "source", "error_type", "error"], + "title": "DownloadErrorEvent", + "type": "object" + }, + "DownloadJob": { + "properties": { + "id": { + "type": "integer", + "title": "Id", + "description": "Numeric ID of this job", + "default": -1 + }, + "dest": { + "type": "string", + "format": "path", + "title": "Dest", + "description": "Initial destination of downloaded model on local disk; a directory or file path" + }, + "download_path": { + "anyOf": [ + { + "type": "string", + "format": "path" + }, + { + "type": "null" + } + ], + "title": "Download Path", + "description": "Final location of downloaded file or directory" + }, + "status": { + "$ref": "#/components/schemas/DownloadJobStatus", + "description": "Status of the download", + "default": "waiting" + }, + "bytes": { + "type": "integer", + "title": "Bytes", + "description": "Bytes downloaded so far", + "default": 0 + }, + "total_bytes": { + "type": "integer", + "title": "Total Bytes", + "description": "Total file size (bytes)", + "default": 0 + }, + "error_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Type", + "description": "Name of exception that caused an error" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error", + "description": "Traceback of the exception that caused an error" + }, + "source": { + "type": "string", + "minLength": 1, + "format": "uri", + "title": "Source", + "description": "Where to download from. Specific types specified in child classes." + }, + "access_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Access Token", + "description": "authorization token for protected resources" + }, + "priority": { + "type": "integer", + "title": "Priority", + "description": "Queue priority; lower values are higher priority", + "default": 10 + }, + "job_started": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Job Started", + "description": "Timestamp for when the download job started" + }, + "job_ended": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Job Ended", + "description": "Timestamp for when the download job ende1d (completed or errored)" + }, + "content_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Content Type", + "description": "Content type of downloaded file" + }, + "canonical_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Canonical Url", + "description": "Canonical URL to request on resume" + }, + "etag": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Etag", + "description": "ETag from the remote server, if available" + }, + "last_modified": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Modified", + "description": "Last-Modified from the remote server, if available" + }, + "final_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Final Url", + "description": "Final resolved URL after redirects, if available" + }, + "expected_total_bytes": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Expected Total Bytes", + "description": "Expected total size of the download" + }, + "resume_required": { + "type": "boolean", + "title": "Resume Required", + "description": "True if server refused resume; restart required", + "default": false + }, + "resume_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Resume Message", + "description": "Message explaining why resume is required" + }, + "resume_from_scratch": { + "type": "boolean", + "title": "Resume From Scratch", + "description": "True if resume metadata existed but the partial file was missing and the download restarted from the beginning", + "default": false + } + }, + "type": "object", + "required": ["dest", "source"], + "title": "DownloadJob", + "description": "Class to monitor and control a model download request." + }, + "DownloadJobStatus": { + "type": "string", + "enum": ["waiting", "running", "paused", "completed", "cancelled", "error"], + "title": "DownloadJobStatus", + "description": "State of a download job." + }, + "DownloadPausedEvent": { + "description": "Event model for download_paused", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "source": { + "description": "The source of the download", + "title": "Source", + "type": "string" + } + }, + "required": ["timestamp", "source"], + "title": "DownloadPausedEvent", + "type": "object" + }, + "DownloadProgressEvent": { + "description": "Event model for download_progress", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "source": { + "description": "The source of the download", + "title": "Source", + "type": "string" + }, + "download_path": { + "description": "The local path where the download is saved", + "title": "Download Path", + "type": "string" + }, + "current_bytes": { + "description": "The number of bytes downloaded so far", + "title": "Current Bytes", + "type": "integer" + }, + "total_bytes": { + "description": "The total number of bytes to be downloaded", + "title": "Total Bytes", + "type": "integer" + } + }, + "required": ["timestamp", "source", "download_path", "current_bytes", "total_bytes"], + "title": "DownloadProgressEvent", + "type": "object" + }, + "DownloadStartedEvent": { + "description": "Event model for download_started", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "source": { + "description": "The source of the download", + "title": "Source", + "type": "string" + }, + "download_path": { + "description": "The local path where the download is saved", + "title": "Download Path", + "type": "string" + } + }, + "required": ["timestamp", "source", "download_path"], + "title": "DownloadStartedEvent", + "type": "object" + }, + "DynamicPromptInvocation": { + "category": "prompt", + "class": "invocation", + "classification": "stable", + "description": "Parses a prompt using adieyal/dynamicprompts' random or combinatorial generator", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": false, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The prompt to parse with dynamicprompts", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Prompt", + "ui_component": "textarea" + }, + "max_prompts": { + "default": 1, + "description": "The number of prompts to generate", + "field_kind": "input", + "input": "any", + "orig_default": 1, + "orig_required": false, + "title": "Max Prompts", + "type": "integer" + }, + "combinatorial": { + "default": false, + "description": "Whether to use the combinatorial generator", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Combinatorial", + "type": "boolean" + }, + "type": { + "const": "dynamic_prompt", + "default": "dynamic_prompt", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["prompt", "collection"], + "title": "Dynamic Prompt", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/StringCollectionOutput" + } + }, + "DynamicPromptsResponse": { + "properties": { + "prompts": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Prompts" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + } + }, + "type": "object", + "required": ["prompts"], + "title": "DynamicPromptsResponse" + }, + "ESRGANInvocation": { + "category": "upscale", + "class": "invocation", + "classification": "stable", + "description": "Upscales an image using RealESRGAN.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The input image", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "model_name": { + "default": "RealESRGAN_x4plus.pth", + "description": "The Real-ESRGAN model to use", + "enum": [ + "RealESRGAN_x4plus.pth", + "RealESRGAN_x4plus_anime_6B.pth", + "ESRGAN_SRx4_DF2KOST_official-ff704c30.pth", + "RealESRGAN_x2plus.pth" + ], + "field_kind": "input", + "input": "any", + "orig_default": "RealESRGAN_x4plus.pth", + "orig_required": false, + "title": "Model Name", + "type": "string" + }, + "tile_size": { + "default": 400, + "description": "Tile size for tiled ESRGAN upscaling (0=tiling disabled)", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 400, + "orig_required": false, + "title": "Tile Size", + "type": "integer" + }, + "type": { + "const": "esrgan", + "default": "esrgan", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["esrgan", "upscale"], + "title": "Upscale (RealESRGAN)", + "type": "object", + "version": "1.3.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "Edge": { + "properties": { + "source": { + "$ref": "#/components/schemas/EdgeConnection", + "description": "The connection for the edge's from node and field" + }, + "destination": { + "$ref": "#/components/schemas/EdgeConnection", + "description": "The connection for the edge's to node and field" + } + }, + "type": "object", + "required": ["source", "destination"], + "title": "Edge" + }, + "EdgeConnection": { + "properties": { + "node_id": { + "type": "string", + "title": "Node Id", + "description": "The id of the node for this edge connection" + }, + "field": { + "type": "string", + "title": "Field", + "description": "The field for this connection" + } + }, + "type": "object", + "required": ["node_id", "field"], + "title": "EdgeConnection" + }, + "EnqueueBatchResult": { + "properties": { + "queue_id": { + "type": "string", + "title": "Queue Id", + "description": "The ID of the queue" + }, + "enqueued": { + "type": "integer", + "title": "Enqueued", + "description": "The total number of queue items enqueued" + }, + "requested": { + "type": "integer", + "title": "Requested", + "description": "The total number of queue items requested to be enqueued" + }, + "batch": { + "$ref": "#/components/schemas/Batch", + "description": "The batch that was enqueued" + }, + "priority": { + "type": "integer", + "title": "Priority", + "description": "The priority of the enqueued batch" + }, + "item_ids": { + "items": { + "type": "integer" + }, + "type": "array", + "title": "Item Ids", + "description": "The IDs of the queue items that were enqueued" + } + }, + "type": "object", + "required": ["queue_id", "enqueued", "requested", "batch", "priority", "item_ids"], + "title": "EnqueueBatchResult" + }, + "ExpandMaskWithFadeInvocation": { + "category": "mask", + "class": "invocation", + "classification": "stable", + "description": "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.\nThe mask is thresholded to create a binary mask, and then a distance transform is applied to create a fade effect.\nThe 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.\nIf the fade size is 0, the mask is returned as-is.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The mask to expand", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "threshold": { + "default": 0, + "description": "The threshold for the binary mask (0-255)", + "field_kind": "input", + "input": "any", + "maximum": 255, + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Threshold", + "type": "integer" + }, + "fade_size_px": { + "default": 32, + "description": "The size of the fade in pixels", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 32, + "orig_required": false, + "title": "Fade Size Px", + "type": "integer" + }, + "type": { + "const": "expand_mask_with_fade", + "default": "expand_mask_with_fade", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "mask"], + "title": "Expand Mask with Fade", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ExpandPromptRequest": { + "properties": { + "prompt": { + "type": "string", + "title": "Prompt" + }, + "model_key": { + "type": "string", + "title": "Model Key" + }, + "max_tokens": { + "type": "integer", + "maximum": 2048.0, + "minimum": 1.0, + "title": "Max Tokens", + "default": 300 + }, + "system_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "System Prompt" + } + }, + "type": "object", + "required": ["prompt", "model_key"], + "title": "ExpandPromptRequest" + }, + "ExpandPromptResponse": { + "properties": { + "expanded_prompt": { + "type": "string", + "title": "Expanded Prompt" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + } + }, + "type": "object", + "required": ["expanded_prompt"], + "title": "ExpandPromptResponse" + }, + "ExposedField": { + "properties": { + "nodeId": { + "type": "string", + "title": "Nodeid" + }, + "fieldName": { + "type": "string", + "title": "Fieldname" + } + }, + "type": "object", + "required": ["nodeId", "fieldName"], + "title": "ExposedField" + }, + "ExternalApiModelConfig": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "default": "" + }, + "path": { + "type": "string", + "title": "Path", + "default": "" + }, + "file_size": { + "type": "integer", + "minimum": 0.0, + "title": "File Size", + "default": 0 + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "default": "" + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "default": "external" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "base": { + "type": "string", + "const": "external", + "title": "Base", + "default": "external" + }, + "type": { + "type": "string", + "const": "external_image_generator", + "title": "Type", + "default": "external_image_generator" + }, + "format": { + "type": "string", + "const": "external_api", + "title": "Format", + "default": "external_api" + }, + "provider_id": { + "type": "string", + "minLength": 1, + "title": "Provider Id", + "description": "External provider ID" + }, + "provider_model_id": { + "type": "string", + "minLength": 1, + "title": "Provider Model Id", + "description": "Provider-specific model ID" + }, + "capabilities": { + "$ref": "#/components/schemas/ExternalModelCapabilities", + "description": "Provider capability matrix" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExternalApiModelDefaultSettings" + }, + { + "type": "null" + } + ] + }, + "panel_schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExternalModelPanelSchema" + }, + { + "type": "null" + } + ] + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags" + }, + "is_default": { + "type": "boolean", + "title": "Is Default", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "base", + "type", + "format", + "provider_id", + "provider_model_id", + "capabilities", + "default_settings", + "panel_schema", + "tags", + "is_default" + ], + "title": "ExternalApiModelConfig" + }, + "ExternalApiModelDefaultSettings": { + "properties": { + "width": { + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Width" + }, + "height": { + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Height" + }, + "num_images": { + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Num Images" + } + }, + "additionalProperties": false, + "type": "object", + "title": "ExternalApiModelDefaultSettings" + }, + "ExternalImageSize": { + "properties": { + "width": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Width" + }, + "height": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Height" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["width", "height"], + "title": "ExternalImageSize" + }, + "ExternalModelCapabilities": { + "properties": { + "modes": { + "items": { + "type": "string", + "enum": ["txt2img", "img2img", "inpaint"] + }, + "type": "array", + "title": "Modes" + }, + "supports_reference_images": { + "type": "boolean", + "title": "Supports Reference Images", + "default": false + }, + "supports_negative_prompt": { + "type": "boolean", + "title": "Supports Negative Prompt", + "default": true + }, + "supports_seed": { + "type": "boolean", + "title": "Supports Seed", + "default": false + }, + "supports_guidance": { + "type": "boolean", + "title": "Supports Guidance", + "default": false + }, + "supports_steps": { + "type": "boolean", + "title": "Supports Steps", + "default": false + }, + "max_images_per_request": { + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Max Images Per Request" + }, + "max_image_size": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExternalImageSize" + }, + { + "type": "null" + } + ] + }, + "allowed_aspect_ratios": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Allowed Aspect Ratios" + }, + "aspect_ratio_sizes": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/ExternalImageSize" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Aspect Ratio Sizes" + }, + "resolution_presets": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ExternalResolutionPreset" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Resolution Presets" + }, + "max_reference_images": { + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Max Reference Images" + }, + "mask_format": { + "type": "string", + "enum": ["alpha", "binary", "none"], + "title": "Mask Format", + "default": "none" + }, + "input_image_required_for": { + "anyOf": [ + { + "items": { + "type": "string", + "enum": ["txt2img", "img2img", "inpaint"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Input Image Required For" + } + }, + "additionalProperties": false, + "type": "object", + "title": "ExternalModelCapabilities" + }, + "ExternalModelPanelControl": { + "properties": { + "name": { + "type": "string", + "enum": ["reference_images", "dimensions", "seed"], + "title": "Name" + }, + "slider_min": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Slider Min" + }, + "slider_max": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Slider Max" + }, + "number_input_min": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Number Input Min" + }, + "number_input_max": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Number Input Max" + }, + "fine_step": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Fine Step" + }, + "coarse_step": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Coarse Step" + }, + "marks": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Marks" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["name"], + "title": "ExternalModelPanelControl" + }, + "ExternalModelPanelSchema": { + "properties": { + "prompts": { + "items": { + "$ref": "#/components/schemas/ExternalModelPanelControl" + }, + "type": "array", + "title": "Prompts" + }, + "image": { + "items": { + "$ref": "#/components/schemas/ExternalModelPanelControl" + }, + "type": "array", + "title": "Image" + }, + "generation": { + "items": { + "$ref": "#/components/schemas/ExternalModelPanelControl" + }, + "type": "array", + "title": "Generation" + } + }, + "additionalProperties": false, + "type": "object", + "title": "ExternalModelPanelSchema" + }, + "ExternalModelSource": { + "properties": { + "provider_id": { + "type": "string", + "title": "Provider Id" + }, + "provider_model_id": { + "type": "string", + "title": "Provider Model Id" + }, + "type": { + "type": "string", + "const": "external", + "title": "Type", + "default": "external" + } + }, + "type": "object", + "required": ["provider_id", "provider_model_id"], + "title": "ExternalModelSource", + "description": "An external provider model identifier." + }, + "ExternalProviderConfigModel": { + "properties": { + "provider_id": { + "type": "string", + "title": "Provider Id", + "description": "The external provider identifier" + }, + "api_key_configured": { + "type": "boolean", + "title": "Api Key Configured", + "description": "Whether an API key is configured" + }, + "base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Url", + "description": "Optional base URL override" + } + }, + "type": "object", + "required": ["provider_id", "api_key_configured"], + "title": "ExternalProviderConfigModel" + }, + "ExternalProviderConfigUpdate": { + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Api Key", + "description": "API key for the external provider" + }, + "base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Url", + "description": "Optional base URL override for the provider" + } + }, + "type": "object", + "title": "ExternalProviderConfigUpdate" + }, + "ExternalProviderStatusModel": { + "properties": { + "provider_id": { + "type": "string", + "title": "Provider Id", + "description": "The external provider identifier" + }, + "configured": { + "type": "boolean", + "title": "Configured", + "description": "Whether credentials are configured for the provider" + }, + "message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Message", + "description": "Optional provider status detail" + } + }, + "type": "object", + "required": ["provider_id", "configured"], + "title": "ExternalProviderStatusModel" + }, + "ExternalResolutionPreset": { + "properties": { + "label": { + "type": "string", + "minLength": 1, + "title": "Label", + "description": "Display label, e.g. '1:1 (1K)'" + }, + "aspect_ratio": { + "type": "string", + "minLength": 1, + "title": "Aspect Ratio", + "description": "Aspect ratio string, e.g. '1:1'" + }, + "image_size": { + "type": "string", + "minLength": 1, + "title": "Image Size", + "description": "Image size preset, e.g. '1K'" + }, + "width": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Width" + }, + "height": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Height" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["label", "aspect_ratio", "image_size", "width", "height"], + "title": "ExternalResolutionPreset" + }, + "FLUXLoRACollectionLoader": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Applies a collection of LoRAs to a FLUX transformer.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "loras": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoRAField" + }, + { + "items": { + "$ref": "#/components/schemas/LoRAField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LoRA models and weights. May be a single LoRA or collection.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "LoRAs" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Transformer", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Transformer" + }, + "clip": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "CLIP" + }, + "t5_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/T5EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "T5 tokenizer and text encoder", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "T5 Encoder" + }, + "type": { + "const": "flux_lora_collection_loader", + "default": "flux_lora_collection_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["lora", "model", "flux"], + "title": "Apply LoRA Collection - FLUX", + "type": "object", + "version": "1.3.1", + "output": { + "$ref": "#/components/schemas/FluxLoRALoaderOutput" + } + }, + "FLUXRedux_Checkpoint_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "flux_redux", + "title": "Type", + "default": "flux_redux" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "base": { + "type": "string", + "const": "flux", + "title": "Base", + "default": "flux" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "format", + "base" + ], + "title": "FLUXRedux_Checkpoint_Config", + "description": "Model config for FLUX Tools Redux model." + }, + "FaceIdentifierInvocation": { + "category": "segmentation", + "class": "invocation", + "classification": "stable", + "description": "Outputs an image with detected face IDs printed on each face. For use with other FaceTools.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Image to face detect", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "minimum_confidence": { + "default": 0.5, + "description": "Minimum confidence for face detection (lower if detection is failing)", + "field_kind": "input", + "input": "any", + "orig_default": 0.5, + "orig_required": false, + "title": "Minimum Confidence", + "type": "number" + }, + "chunk": { + "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.", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Chunk", + "type": "boolean" + }, + "type": { + "const": "face_identifier", + "default": "face_identifier", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "face", "identifier"], + "title": "FaceIdentifier", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "FaceMaskInvocation": { + "category": "segmentation", + "class": "invocation", + "classification": "stable", + "description": "Face mask creation using mediapipe face detection", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Image to face detect", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "face_ids": { + "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.", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Face Ids", + "type": "string" + }, + "minimum_confidence": { + "default": 0.5, + "description": "Minimum confidence for face detection (lower if detection is failing)", + "field_kind": "input", + "input": "any", + "orig_default": 0.5, + "orig_required": false, + "title": "Minimum Confidence", + "type": "number" + }, + "x_offset": { + "default": 0.0, + "description": "Offset for the X-axis of the face mask", + "field_kind": "input", + "input": "any", + "orig_default": 0.0, + "orig_required": false, + "title": "X Offset", + "type": "number" + }, + "y_offset": { + "default": 0.0, + "description": "Offset for the Y-axis of the face mask", + "field_kind": "input", + "input": "any", + "orig_default": 0.0, + "orig_required": false, + "title": "Y Offset", + "type": "number" + }, + "chunk": { + "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.", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Chunk", + "type": "boolean" + }, + "invert_mask": { + "default": false, + "description": "Toggle to invert the mask", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Invert Mask", + "type": "boolean" + }, + "type": { + "const": "face_mask_detection", + "default": "face_mask_detection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "face", "mask"], + "title": "FaceMask", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/FaceMaskOutput" + } + }, + "FaceMaskOutput": { + "class": "output", + "description": "Base class for FaceMask output", + "properties": { + "image": { + "$ref": "#/components/schemas/ImageField", + "description": "The output image", + "field_kind": "output", + "ui_hidden": false + }, + "width": { + "description": "The width of the image in pixels", + "field_kind": "output", + "title": "Width", + "type": "integer", + "ui_hidden": false + }, + "height": { + "description": "The height of the image in pixels", + "field_kind": "output", + "title": "Height", + "type": "integer", + "ui_hidden": false + }, + "type": { + "const": "face_mask_output", + "default": "face_mask_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + }, + "mask": { + "$ref": "#/components/schemas/ImageField", + "description": "The output mask", + "field_kind": "output", + "ui_hidden": false + } + }, + "required": ["output_meta", "image", "width", "height", "type", "mask", "type"], + "title": "FaceMaskOutput", + "type": "object" + }, + "FaceOffInvocation": { + "category": "segmentation", + "class": "invocation", + "classification": "stable", + "description": "Bound, extract, and mask a face from an image using MediaPipe detection", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Image for face detection", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "face_id": { + "default": 0, + "description": "The face ID to process, numbered from 0. Multiple faces not supported. Find a face's ID with FaceIdentifier node.", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Face Id", + "type": "integer" + }, + "minimum_confidence": { + "default": 0.5, + "description": "Minimum confidence for face detection (lower if detection is failing)", + "field_kind": "input", + "input": "any", + "orig_default": 0.5, + "orig_required": false, + "title": "Minimum Confidence", + "type": "number" + }, + "x_offset": { + "default": 0.0, + "description": "X-axis offset of the mask", + "field_kind": "input", + "input": "any", + "orig_default": 0.0, + "orig_required": false, + "title": "X Offset", + "type": "number" + }, + "y_offset": { + "default": 0.0, + "description": "Y-axis offset of the mask", + "field_kind": "input", + "input": "any", + "orig_default": 0.0, + "orig_required": false, + "title": "Y Offset", + "type": "number" + }, + "padding": { + "default": 0, + "description": "All-axis padding around the mask in pixels", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Padding", + "type": "integer" + }, + "chunk": { + "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.", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Chunk", + "type": "boolean" + }, + "type": { + "const": "face_off", + "default": "face_off", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "faceoff", "face", "mask"], + "title": "FaceOff", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/FaceOffOutput" + } + }, + "FaceOffOutput": { + "class": "output", + "description": "Base class for FaceOff Output", + "properties": { + "image": { + "$ref": "#/components/schemas/ImageField", + "description": "The output image", + "field_kind": "output", + "ui_hidden": false + }, + "width": { + "description": "The width of the image in pixels", + "field_kind": "output", + "title": "Width", + "type": "integer", + "ui_hidden": false + }, + "height": { + "description": "The height of the image in pixels", + "field_kind": "output", + "title": "Height", + "type": "integer", + "ui_hidden": false + }, + "type": { + "const": "face_off_output", + "default": "face_off_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + }, + "mask": { + "$ref": "#/components/schemas/ImageField", + "description": "The output mask", + "field_kind": "output", + "ui_hidden": false + }, + "x": { + "description": "The x coordinate of the bounding box's left side", + "field_kind": "output", + "title": "X", + "type": "integer", + "ui_hidden": false + }, + "y": { + "description": "The y coordinate of the bounding box's top side", + "field_kind": "output", + "title": "Y", + "type": "integer", + "ui_hidden": false + } + }, + "required": ["output_meta", "image", "width", "height", "type", "mask", "x", "y", "type"], + "title": "FaceOffOutput", + "type": "object" + }, + "FieldKind": { + "description": "The kind of field.\n- `Input`: An input field on a node.\n- `Output`: An output field on a node.\n- `Internal`: A field which is treated as an input, but cannot be used in node definitions. Metadata is\none example. It is provided to nodes via the WithMetadata class, and we want to reserve the field name\n\"metadata\" for this on all nodes. `FieldKind` is used to short-circuit the field name validation logic,\nallowing \"metadata\" for that field.\n- `NodeAttribute`: The field is a node attribute. These are fields which are not inputs or outputs,\nbut which are used to store information about the node. For example, the `id` and `type` fields are node\nattributes.\n\nThe presence of this in `json_schema_extra[\"field_kind\"]` is used when initializing node schemas on app\nstartup, and when generating the OpenAPI schema for the workflow editor.", + "enum": ["input", "output", "internal", "node_attribute"], + "title": "FieldKind", + "type": "string" + }, + "FloatBatchInvocation": { + "category": "batch", + "class": "invocation", + "classification": "special", + "description": "Create a batched generation, where the workflow is executed once for each float in the batch.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "batch_group_id": { + "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.", + "enum": ["None", "Group 1", "Group 2", "Group 3", "Group 4", "Group 5"], + "field_kind": "input", + "input": "direct", + "orig_default": "None", + "orig_required": false, + "title": "Batch Group", + "type": "string" + }, + "floats": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "minItems": 1, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The floats to batch over", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Floats" + }, + "type": { + "const": "float_batch", + "default": "float_batch", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "float", "number", "batch", "special"], + "title": "Float Batch", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/FloatOutput" + } + }, + "FloatCollectionInvocation": { + "category": "primitives", + "class": "invocation", + "classification": "stable", + "description": "A collection of float primitive values", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "collection": { + "default": [], + "description": "The collection of float values", + "field_kind": "input", + "input": "any", + "items": { + "type": "number" + }, + "orig_default": [], + "orig_required": false, + "title": "Collection", + "type": "array" + }, + "type": { + "const": "float_collection", + "default": "float_collection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "float", "collection"], + "title": "Float Collection Primitive", + "type": "object", + "version": "1.0.2", + "output": { + "$ref": "#/components/schemas/FloatCollectionOutput" + } + }, + "FloatCollectionOutput": { + "class": "output", + "description": "Base class for nodes that output a collection of floats", + "properties": { + "collection": { + "description": "The float collection", + "field_kind": "output", + "items": { + "type": "number" + }, + "title": "Collection", + "type": "array", + "ui_hidden": false + }, + "type": { + "const": "float_collection_output", + "default": "float_collection_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "collection", "type", "type"], + "title": "FloatCollectionOutput", + "type": "object" + }, + "FloatGenerator": { + "category": "batch", + "class": "invocation", + "classification": "special", + "description": "Generated a range of floats for use in a batched generation", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "generator": { + "$ref": "#/components/schemas/FloatGeneratorField", + "description": "The float generator.", + "field_kind": "input", + "input": "direct", + "orig_required": true, + "title": "Generator Type" + }, + "type": { + "const": "float_generator", + "default": "float_generator", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["generator", "type", "id"], + "tags": ["primitives", "float", "number", "batch", "special"], + "title": "Float Generator", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/FloatGeneratorOutput" + } + }, + "FloatGeneratorField": { + "properties": {}, + "title": "FloatGeneratorField", + "type": "object" + }, + "FloatGeneratorOutput": { + "class": "output", + "description": "Base class for nodes that output a collection of floats", + "properties": { + "floats": { + "description": "The generated floats", + "field_kind": "output", + "items": { + "type": "number" + }, + "title": "Floats", + "type": "array", + "ui_hidden": false + }, + "type": { + "const": "float_generator_output", + "default": "float_generator_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "floats", "type", "type"], + "title": "FloatGeneratorOutput", + "type": "object" + }, + "FloatInvocation": { + "category": "primitives", + "class": "invocation", + "classification": "stable", + "description": "A float primitive value", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "value": { + "default": 0.0, + "description": "The float value", + "field_kind": "input", + "input": "any", + "orig_default": 0.0, + "orig_required": false, + "title": "Value", + "type": "number" + }, + "type": { + "const": "float", + "default": "float", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "float"], + "title": "Float Primitive", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/FloatOutput" + } + }, + "FloatLinearRangeInvocation": { + "category": "math", + "class": "invocation", + "classification": "stable", + "description": "Creates a range", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "start": { + "default": 5, + "description": "The first value of the range", + "field_kind": "input", + "input": "any", + "orig_default": 5, + "orig_required": false, + "title": "Start", + "type": "number" + }, + "stop": { + "default": 10, + "description": "The last value of the range", + "field_kind": "input", + "input": "any", + "orig_default": 10, + "orig_required": false, + "title": "Stop", + "type": "number" + }, + "steps": { + "default": 30, + "description": "number of values to interpolate over (including start and stop)", + "field_kind": "input", + "input": "any", + "orig_default": 30, + "orig_required": false, + "title": "Steps", + "type": "integer" + }, + "type": { + "const": "float_range", + "default": "float_range", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["math", "range"], + "title": "Float Range", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/FloatCollectionOutput" + } + }, + "FloatMathInvocation": { + "category": "math", + "class": "invocation", + "classification": "stable", + "description": "Performs floating point math.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "operation": { + "default": "ADD", + "description": "The operation to perform", + "enum": ["ADD", "SUB", "MUL", "DIV", "EXP", "ABS", "SQRT", "MIN", "MAX"], + "field_kind": "input", + "input": "any", + "orig_default": "ADD", + "orig_required": false, + "title": "Operation", + "type": "string", + "ui_choice_labels": { + "ABS": "Absolute Value of A", + "ADD": "Add A+B", + "DIV": "Divide A/B", + "EXP": "Exponentiate A^B", + "MAX": "Maximum(A,B)", + "MIN": "Minimum(A,B)", + "MUL": "Multiply A*B", + "SQRT": "Square Root of A", + "SUB": "Subtract A-B" + } + }, + "a": { + "default": 1, + "description": "The first number", + "field_kind": "input", + "input": "any", + "orig_default": 1, + "orig_required": false, + "title": "A", + "type": "number" + }, + "b": { + "default": 1, + "description": "The second number", + "field_kind": "input", + "input": "any", + "orig_default": 1, + "orig_required": false, + "title": "B", + "type": "number" + }, + "type": { + "const": "float_math", + "default": "float_math", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": [ + "math", + "float", + "add", + "subtract", + "multiply", + "divide", + "power", + "root", + "absolute value", + "min", + "max" + ], + "title": "Float Math", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/FloatOutput" + } + }, + "FloatOutput": { + "class": "output", + "description": "Base class for nodes that output a single float", + "properties": { + "value": { + "description": "The output float", + "field_kind": "output", + "title": "Value", + "type": "number", + "ui_hidden": false + }, + "type": { + "const": "float_output", + "default": "float_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "value", "type", "type"], + "title": "FloatOutput", + "type": "object" + }, + "FloatToIntegerInvocation": { + "category": "math", + "class": "invocation", + "classification": "stable", + "description": "Rounds a float number to (a multiple of) an integer.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "value": { + "default": 0, + "description": "The value to round", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Value", + "type": "number" + }, + "multiple": { + "default": 1, + "description": "The multiple to round to", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 1, + "orig_required": false, + "title": "Multiple of", + "type": "integer" + }, + "method": { + "default": "Nearest", + "description": "The method to use for rounding", + "enum": ["Nearest", "Floor", "Ceiling", "Truncate"], + "field_kind": "input", + "input": "any", + "orig_default": "Nearest", + "orig_required": false, + "title": "Method", + "type": "string" + }, + "type": { + "const": "float_to_int", + "default": "float_to_int", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["math", "round", "integer", "float", "convert"], + "title": "Float To Integer", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/IntegerOutput" + } + }, + "Flux2DenoiseInvocation": { + "category": "latents", + "class": "invocation", + "classification": "prototype", + "description": "Run denoising process with a FLUX.2 Klein transformer model.\n\nThis node is designed for FLUX.2 Klein models which use Qwen3 as the text encoder.\nIt does not support ControlNet, IP-Adapters, or regional prompting.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "noise": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Noise tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoise_mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/DenoiseMaskField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "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.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoising_start": { + "default": 0.0, + "description": "When to start denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0.0, + "orig_required": false, + "title": "Denoising Start", + "type": "number" + }, + "denoising_end": { + "default": 1.0, + "description": "When to stop denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Denoising End", + "type": "number" + }, + "add_noise": { + "default": true, + "description": "Add noise based on denoising start.", + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "Add Noise", + "type": "boolean" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Flux model (Transformer) to load", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Transformer" + }, + "positive_text_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/FluxConditioningField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Positive conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "negative_text_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/FluxConditioningField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Negative conditioning tensor. Can be None if cfg_scale is 1.0.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "guidance": { + "default": 4.0, + "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.", + "field_kind": "input", + "input": "any", + "maximum": 20, + "minimum": 0, + "orig_default": 4.0, + "orig_required": false, + "title": "Guidance", + "type": "number" + }, + "cfg_scale": { + "default": 1.0, + "description": "Classifier-Free Guidance scale", + "field_kind": "input", + "input": "any", + "orig_default": 1.0, + "orig_required": false, + "title": "CFG Scale", + "type": "number" + }, + "width": { + "default": 1024, + "description": "Width of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 16, + "orig_default": 1024, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 1024, + "description": "Height of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 16, + "orig_default": 1024, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "num_steps": { + "default": 4, + "description": "Number of diffusion steps. Use 4 for distilled models, 28+ for base models.", + "field_kind": "input", + "input": "any", + "orig_default": 4, + "orig_required": false, + "title": "Num Steps", + "type": "integer" + }, + "scheduler": { + "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.", + "enum": ["euler", "heun", "lcm"], + "field_kind": "input", + "input": "any", + "orig_default": "euler", + "orig_required": false, + "title": "Scheduler", + "type": "string", + "ui_choice_labels": { + "euler": "Euler", + "heun": "Heun (2nd order)", + "lcm": "LCM" + } + }, + "seed": { + "default": 0, + "description": "Randomness seed for reproducibility.", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Seed", + "type": "integer" + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "FLUX.2 VAE model (required for BN statistics).", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "kontext_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/FluxKontextConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/FluxKontextConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "FLUX Kontext conditioning (reference images for multi-reference image editing).", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Reference Images" + }, + "type": { + "const": "flux2_denoise", + "default": "flux2_denoise", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "flux", "flux2", "klein", "denoise"], + "title": "FLUX2 Denoise", + "type": "object", + "version": "1.5.0", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "Flux2KleinLoRACollectionLoader": { + "category": "model", + "class": "invocation", + "classification": "prototype", + "description": "Applies a collection of LoRAs to a FLUX.2 Klein transformer and/or Qwen3 text encoder.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "loras": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoRAField" + }, + { + "items": { + "$ref": "#/components/schemas/LoRAField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LoRA models and weights. May be a single LoRA or collection.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "LoRAs" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Transformer", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Transformer" + }, + "qwen3_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/Qwen3EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Qwen3 tokenizer and text encoder", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Qwen3 Encoder" + }, + "type": { + "const": "flux2_klein_lora_collection_loader", + "default": "flux2_klein_lora_collection_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["lora", "model", "flux", "klein", "flux2"], + "title": "Apply LoRA Collection - Flux2 Klein", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/Flux2KleinLoRALoaderOutput" + } + }, + "Flux2KleinLoRALoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "prototype", + "description": "Apply a LoRA model to a FLUX.2 Klein transformer and/or Qwen3 text encoder.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "lora": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LoRA model to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "LoRA", + "ui_model_base": ["flux2"], + "ui_model_type": ["lora"] + }, + "weight": { + "default": 0.75, + "description": "The weight at which the LoRA is applied to each model", + "field_kind": "input", + "input": "any", + "orig_default": 0.75, + "orig_required": false, + "title": "Weight", + "type": "number" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Transformer", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Transformer" + }, + "qwen3_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/Qwen3EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Qwen3 tokenizer and text encoder", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Qwen3 Encoder" + }, + "type": { + "const": "flux2_klein_lora_loader", + "default": "flux2_klein_lora_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["lora", "model", "flux", "klein", "flux2"], + "title": "Apply LoRA - Flux2 Klein", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/Flux2KleinLoRALoaderOutput" + } + }, + "Flux2KleinLoRALoaderOutput": { + "class": "output", + "description": "FLUX.2 Klein LoRA Loader Output", + "properties": { + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Transformer", + "field_kind": "output", + "title": "Transformer", + "ui_hidden": false + }, + "qwen3_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/Qwen3EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Qwen3 tokenizer and text encoder", + "field_kind": "output", + "title": "Qwen3 Encoder", + "ui_hidden": false + }, + "type": { + "const": "flux2_klein_lora_loader_output", + "default": "flux2_klein_lora_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "transformer", "qwen3_encoder", "type", "type"], + "title": "Flux2KleinLoRALoaderOutput", + "type": "object" + }, + "Flux2KleinModelLoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "prototype", + "description": "Loads a Flux2 Klein model, outputting its submodels.\n\nFlux2 Klein uses Qwen3 as the text encoder instead of CLIP+T5.\nIt uses a 32-channel VAE (AutoencoderKLFlux2) instead of the 16-channel FLUX.1 VAE.\n\nWhen using a Diffusers format model, both VAE and Qwen3 encoder are extracted\nautomatically from the main model. You can override with standalone models:\n- Transformer: Always from Flux2 Klein main model\n- VAE: From main model (Diffusers) or standalone VAE\n- Qwen3 Encoder: From main model (Diffusers) or standalone Qwen3 model", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Flux model (Transformer) to load", + "field_kind": "input", + "input": "direct", + "orig_required": true, + "title": "Transformer", + "ui_model_base": ["flux2"], + "ui_model_type": ["main"] + }, + "vae_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "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.", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "VAE", + "ui_model_base": ["flux", "flux2"], + "ui_model_type": ["vae"] + }, + "qwen3_encoder_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Standalone Qwen3 Encoder model. If not provided, encoder will be loaded from the Qwen3 Source model.", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Qwen3 Encoder", + "ui_model_type": ["qwen3_encoder"] + }, + "qwen3_source_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "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.", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Qwen3 Source (Diffusers)", + "ui_model_base": ["flux2"], + "ui_model_format": ["diffusers"], + "ui_model_type": ["main"] + }, + "max_seq_len": { + "default": 512, + "description": "Max sequence length for the Qwen3 encoder.", + "enum": [256, 512], + "field_kind": "input", + "input": "any", + "orig_default": 512, + "orig_required": false, + "title": "Max Seq Length", + "type": "integer" + }, + "type": { + "const": "flux2_klein_model_loader", + "default": "flux2_klein_model_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["model", "type", "id"], + "tags": ["model", "flux", "klein", "qwen3"], + "title": "Main Model - Flux2 Klein", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/Flux2KleinModelLoaderOutput" + } + }, + "Flux2KleinModelLoaderOutput": { + "class": "output", + "description": "Flux2 Klein model loader output.", + "properties": { + "transformer": { + "$ref": "#/components/schemas/TransformerField", + "description": "Transformer", + "field_kind": "output", + "title": "Transformer", + "ui_hidden": false + }, + "qwen3_encoder": { + "$ref": "#/components/schemas/Qwen3EncoderField", + "description": "Qwen3 tokenizer and text encoder", + "field_kind": "output", + "title": "Qwen3 Encoder", + "ui_hidden": false + }, + "vae": { + "$ref": "#/components/schemas/VAEField", + "description": "VAE", + "field_kind": "output", + "title": "VAE", + "ui_hidden": false + }, + "max_seq_len": { + "description": "The max sequence length for the Qwen3 encoder.", + "enum": [256, 512], + "field_kind": "output", + "title": "Max Seq Length", + "type": "integer", + "ui_hidden": false + }, + "type": { + "const": "flux2_klein_model_loader_output", + "default": "flux2_klein_model_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "transformer", "qwen3_encoder", "vae", "max_seq_len", "type", "type"], + "title": "Flux2KleinModelLoaderOutput", + "type": "object" + }, + "Flux2KleinTextEncoderInvocation": { + "category": "prompt", + "class": "invocation", + "classification": "prototype", + "description": "Encodes and preps a prompt for Flux2 Klein image generation.\n\nFlux2 Klein uses Qwen3 as the text encoder, extracting hidden states from\nlayers (9, 18, 27) and stacking them for richer text representations.\nThis matches the diffusers Flux2KleinPipeline implementation exactly.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Text prompt to encode.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Prompt", + "ui_component": "textarea" + }, + "qwen3_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/Qwen3EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Qwen3 tokenizer and text encoder", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Qwen3 Encoder" + }, + "max_seq_len": { + "default": 512, + "description": "Max sequence length for the Qwen3 encoder.", + "enum": [256, 512], + "field_kind": "input", + "input": "any", + "orig_default": 512, + "orig_required": false, + "title": "Max Seq Len", + "type": "integer" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A mask defining the region that this conditioning prompt applies to.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "type": { + "const": "flux2_klein_text_encoder", + "default": "flux2_klein_text_encoder", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["prompt", "conditioning", "flux", "klein", "qwen3"], + "title": "Prompt - Flux2 Klein", + "type": "object", + "version": "1.1.1", + "output": { + "$ref": "#/components/schemas/FluxConditioningOutput" + } + }, + "Flux2VaeDecodeInvocation": { + "category": "latents", + "class": "invocation", + "classification": "prototype", + "description": "Generates an image from latents using FLUX.2 Klein's 32-channel VAE.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "type": { + "const": "flux2_vae_decode", + "default": "flux2_vae_decode", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "image", "vae", "l2i", "flux2", "klein"], + "title": "Latents to Image - FLUX2", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "Flux2VaeEncodeInvocation": { + "category": "latents", + "class": "invocation", + "classification": "prototype", + "description": "Encodes an image into latents using FLUX.2 Klein's 32-channel VAE.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to encode.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "type": { + "const": "flux2_vae_encode", + "default": "flux2_vae_encode", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "image", "vae", "i2l", "flux2", "klein"], + "title": "Image to Latents - FLUX2", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "Flux2VariantType": { + "type": "string", + "enum": ["klein_4b", "klein_4b_base", "klein_9b", "klein_9b_base"], + "title": "Flux2VariantType", + "description": "FLUX.2 model variants." + }, + "FluxConditioningCollectionOutput": { + "class": "output", + "description": "Base class for nodes that output a collection of conditioning tensors", + "properties": { + "collection": { + "description": "The output conditioning tensors", + "field_kind": "output", + "items": { + "$ref": "#/components/schemas/FluxConditioningField" + }, + "title": "Collection", + "type": "array", + "ui_hidden": false + }, + "type": { + "const": "flux_conditioning_collection_output", + "default": "flux_conditioning_collection_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "collection", "type", "type"], + "title": "FluxConditioningCollectionOutput", + "type": "object" + }, + "FluxConditioningField": { + "description": "A conditioning tensor primitive value", + "properties": { + "conditioning_name": { + "description": "The name of conditioning tensor", + "title": "Conditioning Name", + "type": "string" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The mask associated with this conditioning tensor. Excluded regions should be set to False, included regions should be set to True." + } + }, + "required": ["conditioning_name"], + "title": "FluxConditioningField", + "type": "object" + }, + "FluxConditioningOutput": { + "class": "output", + "description": "Base class for nodes that output a single conditioning tensor", + "properties": { + "conditioning": { + "$ref": "#/components/schemas/FluxConditioningField", + "description": "Conditioning tensor", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "flux_conditioning_output", + "default": "flux_conditioning_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "conditioning", "type", "type"], + "title": "FluxConditioningOutput", + "type": "object" + }, + "FluxControlLoRALoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "LoRA model and Image to use with FLUX transformer generation.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "lora": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Control LoRA model to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Control LoRA", + "ui_model_base": ["flux"], + "ui_model_type": ["control_lora"] + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to encode.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "weight": { + "default": 1.0, + "description": "The weight of the LoRA.", + "field_kind": "input", + "input": "any", + "orig_default": 1.0, + "orig_required": false, + "title": "Weight", + "type": "number" + }, + "type": { + "const": "flux_control_lora_loader", + "default": "flux_control_lora_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["lora", "model", "flux"], + "title": "Control LoRA - FLUX", + "type": "object", + "version": "1.1.1", + "output": { + "$ref": "#/components/schemas/FluxControlLoRALoaderOutput" + } + }, + "FluxControlLoRALoaderOutput": { + "class": "output", + "description": "Flux Control LoRA Loader Output", + "properties": { + "control_lora": { + "$ref": "#/components/schemas/ControlLoRAField", + "default": null, + "description": "Control LoRAs to apply on model loading", + "field_kind": "output", + "title": "Flux Control LoRA", + "ui_hidden": false + }, + "type": { + "const": "flux_control_lora_loader_output", + "default": "flux_control_lora_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "control_lora", "type", "type"], + "title": "FluxControlLoRALoaderOutput", + "type": "object" + }, + "FluxControlNetField": { + "properties": { + "image": { + "$ref": "#/components/schemas/ImageField", + "description": "The control image" + }, + "control_model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "The ControlNet model to use" + }, + "control_weight": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 1, + "description": "The weight given to the ControlNet", + "title": "Control Weight" + }, + "begin_step_percent": { + "default": 0, + "description": "When the ControlNet is first applied (% of total steps)", + "maximum": 1, + "minimum": 0, + "title": "Begin Step Percent", + "type": "number" + }, + "end_step_percent": { + "default": 1, + "description": "When the ControlNet is last applied (% of total steps)", + "maximum": 1, + "minimum": 0, + "title": "End Step Percent", + "type": "number" + }, + "resize_mode": { + "default": "just_resize", + "description": "The resize mode to use", + "enum": ["just_resize", "crop_resize", "fill_resize", "just_resize_simple"], + "title": "Resize Mode", + "type": "string" + }, + "instantx_control_mode": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": -1, + "description": "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'.", + "title": "Instantx Control Mode" + } + }, + "required": ["image", "control_model"], + "title": "FluxControlNetField", + "type": "object" + }, + "FluxControlNetInvocation": { + "category": "conditioning", + "class": "invocation", + "classification": "stable", + "description": "Collect FLUX ControlNet info to pass to other nodes.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The control image", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "control_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "ControlNet model to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "ui_model_base": ["flux"], + "ui_model_type": ["controlnet"] + }, + "control_weight": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 1.0, + "description": "The weight given to the ControlNet", + "field_kind": "input", + "ge": -1, + "input": "any", + "le": 2, + "orig_default": 1.0, + "orig_required": false, + "title": "Control Weight" + }, + "begin_step_percent": { + "default": 0, + "description": "When the ControlNet is first applied (% of total steps)", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Begin Step Percent", + "type": "number" + }, + "end_step_percent": { + "default": 1, + "description": "When the ControlNet is last applied (% of total steps)", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1, + "orig_required": false, + "title": "End Step Percent", + "type": "number" + }, + "resize_mode": { + "default": "just_resize", + "description": "The resize mode used", + "enum": ["just_resize", "crop_resize", "fill_resize", "just_resize_simple"], + "field_kind": "input", + "input": "any", + "orig_default": "just_resize", + "orig_required": false, + "title": "Resize Mode", + "type": "string" + }, + "instantx_control_mode": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": -1, + "description": "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'.", + "field_kind": "input", + "input": "any", + "orig_default": -1, + "orig_required": false, + "title": "Instantx Control Mode" + }, + "type": { + "const": "flux_controlnet", + "default": "flux_controlnet", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["controlnet", "flux"], + "title": "FLUX ControlNet", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/FluxControlNetOutput" + } + }, + "FluxControlNetOutput": { + "class": "output", + "description": "FLUX ControlNet info", + "properties": { + "control": { + "$ref": "#/components/schemas/FluxControlNetField", + "description": "ControlNet(s) to apply", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "flux_controlnet_output", + "default": "flux_controlnet_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "control", "type", "type"], + "title": "FluxControlNetOutput", + "type": "object" + }, + "FluxDenoiseInvocation": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Run denoising process with a FLUX transformer model.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "noise": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Noise tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoise_mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/DenoiseMaskField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "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.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoising_start": { + "default": 0.0, + "description": "When to start denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0.0, + "orig_required": false, + "title": "Denoising Start", + "type": "number" + }, + "denoising_end": { + "default": 1.0, + "description": "When to stop denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Denoising End", + "type": "number" + }, + "add_noise": { + "default": true, + "description": "Add noise based on denoising start.", + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "Add Noise", + "type": "boolean" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Flux model (Transformer) to load", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Transformer" + }, + "control_lora": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlLoRAField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Control LoRA model to load", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Control LoRA" + }, + "positive_text_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/FluxConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/FluxConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Positive conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Positive Text Conditioning" + }, + "negative_text_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/FluxConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/FluxConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Negative conditioning tensor. Can be None if cfg_scale is 1.0.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Negative Text Conditioning" + }, + "redux_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/FluxReduxConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/FluxReduxConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "FLUX Redux conditioning tensor.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Redux Conditioning" + }, + "fill_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/FluxFillConditioningField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "FLUX Fill conditioning.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "cfg_scale": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 1.0, + "description": "Classifier-Free Guidance scale", + "field_kind": "input", + "input": "any", + "orig_default": 1.0, + "orig_required": false, + "title": "CFG Scale" + }, + "cfg_scale_start_step": { + "default": 0, + "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).", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "CFG Scale Start Step", + "type": "integer" + }, + "cfg_scale_end_step": { + "default": -1, + "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).", + "field_kind": "input", + "input": "any", + "orig_default": -1, + "orig_required": false, + "title": "CFG Scale End Step", + "type": "integer" + }, + "width": { + "default": 1024, + "description": "Width of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 16, + "orig_default": 1024, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 1024, + "description": "Height of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 16, + "orig_default": 1024, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "num_steps": { + "default": 4, + "description": "Number of diffusion steps. Recommended values are schnell: 4, dev: 50.", + "field_kind": "input", + "input": "any", + "orig_default": 4, + "orig_required": false, + "title": "Num Steps", + "type": "integer" + }, + "scheduler": { + "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.", + "enum": ["euler", "heun", "lcm"], + "field_kind": "input", + "input": "any", + "orig_default": "euler", + "orig_required": false, + "title": "Scheduler", + "type": "string", + "ui_choice_labels": { + "euler": "Euler", + "heun": "Heun (2nd order)", + "lcm": "LCM" + } + }, + "guidance": { + "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.", + "field_kind": "input", + "input": "any", + "orig_default": 4.0, + "orig_required": false, + "title": "Guidance", + "type": "number" + }, + "seed": { + "default": 0, + "description": "Randomness seed for reproducibility.", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Seed", + "type": "integer" + }, + "control": { + "anyOf": [ + { + "$ref": "#/components/schemas/FluxControlNetField" + }, + { + "items": { + "$ref": "#/components/schemas/FluxControlNetField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "ControlNet models.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Control" + }, + "controlnet_vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "ip_adapter": { + "anyOf": [ + { + "$ref": "#/components/schemas/IPAdapterField" + }, + { + "items": { + "$ref": "#/components/schemas/IPAdapterField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "IP-Adapter to apply", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "IP-Adapter" + }, + "kontext_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/FluxKontextConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/FluxKontextConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "FLUX Kontext conditioning (reference image).", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Kontext Conditioning" + }, + "dype_preset": { + "default": "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.", + "enum": ["off", "manual", "auto", "area", "4k"], + "field_kind": "input", + "input": "any", + "orig_default": "off", + "orig_required": false, + "title": "Dype Preset", + "type": "string", + "ui_choice_labels": { + "4k": "4K Optimized", + "area": "Area (auto)", + "auto": "Auto (>1536px)", + "manual": "Manual", + "off": "Off" + }, + "ui_order": 100 + }, + "dype_scale": { + "anyOf": [ + { + "maximum": 8.0, + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "DyPE magnitude (\u03bbs). Higher values = stronger extrapolation. Only used when dype_preset is not 'off'.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Dype Scale", + "ui_order": 101 + }, + "dype_exponent": { + "anyOf": [ + { + "maximum": 1000.0, + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "DyPE decay speed (\u03bbt). Controls transition from low to high frequency detail. Only used when dype_preset is not 'off'.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Dype Exponent", + "ui_order": 102 + }, + "type": { + "const": "flux_denoise", + "default": "flux_denoise", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "flux"], + "title": "FLUX Denoise", + "type": "object", + "version": "4.6.0", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "FluxDenoiseLatentsMetaInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "stable", + "description": "Run denoising process with a FLUX transformer model + metadata.", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "noise": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Noise tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoise_mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/DenoiseMaskField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "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.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoising_start": { + "default": 0.0, + "description": "When to start denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0.0, + "orig_required": false, + "title": "Denoising Start", + "type": "number" + }, + "denoising_end": { + "default": 1.0, + "description": "When to stop denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Denoising End", + "type": "number" + }, + "add_noise": { + "default": true, + "description": "Add noise based on denoising start.", + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "Add Noise", + "type": "boolean" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Flux model (Transformer) to load", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Transformer" + }, + "control_lora": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlLoRAField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Control LoRA model to load", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Control LoRA" + }, + "positive_text_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/FluxConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/FluxConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Positive conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Positive Text Conditioning" + }, + "negative_text_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/FluxConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/FluxConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Negative conditioning tensor. Can be None if cfg_scale is 1.0.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Negative Text Conditioning" + }, + "redux_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/FluxReduxConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/FluxReduxConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "FLUX Redux conditioning tensor.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Redux Conditioning" + }, + "fill_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/FluxFillConditioningField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "FLUX Fill conditioning.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "cfg_scale": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 1.0, + "description": "Classifier-Free Guidance scale", + "field_kind": "input", + "input": "any", + "orig_default": 1.0, + "orig_required": false, + "title": "CFG Scale" + }, + "cfg_scale_start_step": { + "default": 0, + "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).", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "CFG Scale Start Step", + "type": "integer" + }, + "cfg_scale_end_step": { + "default": -1, + "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).", + "field_kind": "input", + "input": "any", + "orig_default": -1, + "orig_required": false, + "title": "CFG Scale End Step", + "type": "integer" + }, + "width": { + "default": 1024, + "description": "Width of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 16, + "orig_default": 1024, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 1024, + "description": "Height of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 16, + "orig_default": 1024, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "num_steps": { + "default": 4, + "description": "Number of diffusion steps. Recommended values are schnell: 4, dev: 50.", + "field_kind": "input", + "input": "any", + "orig_default": 4, + "orig_required": false, + "title": "Num Steps", + "type": "integer" + }, + "scheduler": { + "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.", + "enum": ["euler", "heun", "lcm"], + "field_kind": "input", + "input": "any", + "orig_default": "euler", + "orig_required": false, + "title": "Scheduler", + "type": "string", + "ui_choice_labels": { + "euler": "Euler", + "heun": "Heun (2nd order)", + "lcm": "LCM" + } + }, + "guidance": { + "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.", + "field_kind": "input", + "input": "any", + "orig_default": 4.0, + "orig_required": false, + "title": "Guidance", + "type": "number" + }, + "seed": { + "default": 0, + "description": "Randomness seed for reproducibility.", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Seed", + "type": "integer" + }, + "control": { + "anyOf": [ + { + "$ref": "#/components/schemas/FluxControlNetField" + }, + { + "items": { + "$ref": "#/components/schemas/FluxControlNetField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "ControlNet models.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Control" + }, + "controlnet_vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "ip_adapter": { + "anyOf": [ + { + "$ref": "#/components/schemas/IPAdapterField" + }, + { + "items": { + "$ref": "#/components/schemas/IPAdapterField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "IP-Adapter to apply", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "IP-Adapter" + }, + "kontext_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/FluxKontextConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/FluxKontextConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "FLUX Kontext conditioning (reference image).", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Kontext Conditioning" + }, + "dype_preset": { + "default": "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.", + "enum": ["off", "manual", "auto", "area", "4k"], + "field_kind": "input", + "input": "any", + "orig_default": "off", + "orig_required": false, + "title": "Dype Preset", + "type": "string", + "ui_choice_labels": { + "4k": "4K Optimized", + "area": "Area (auto)", + "auto": "Auto (>1536px)", + "manual": "Manual", + "off": "Off" + }, + "ui_order": 100 + }, + "dype_scale": { + "anyOf": [ + { + "maximum": 8.0, + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "DyPE magnitude (\u03bbs). Higher values = stronger extrapolation. Only used when dype_preset is not 'off'.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Dype Scale", + "ui_order": 101 + }, + "dype_exponent": { + "anyOf": [ + { + "maximum": 1000.0, + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "DyPE decay speed (\u03bbt). Controls transition from low to high frequency detail. Only used when dype_preset is not 'off'.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Dype Exponent", + "ui_order": 102 + }, + "type": { + "const": "flux_denoise_meta", + "default": "flux_denoise_meta", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["flux", "latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"], + "title": "FLUX Denoise + Metadata", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/LatentsMetaOutput" + } + }, + "FluxFillConditioningField": { + "description": "A FLUX Fill conditioning field.", + "properties": { + "image": { + "$ref": "#/components/schemas/ImageField", + "description": "The FLUX Fill reference image." + }, + "mask": { + "$ref": "#/components/schemas/TensorField", + "description": "The FLUX Fill inpaint mask." + } + }, + "required": ["image", "mask"], + "title": "FluxFillConditioningField", + "type": "object" + }, + "FluxFillInvocation": { + "category": "conditioning", + "class": "invocation", + "classification": "beta", + "description": "Prepare the FLUX Fill conditioning data.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The FLUX Fill reference image.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The bool inpainting mask. Excluded regions should be set to False, included regions should be set to True.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "flux_fill", + "default": "flux_fill", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["inpaint"], + "title": "FLUX Fill Conditioning", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/FluxFillOutput" + } + }, + "FluxFillOutput": { + "class": "output", + "description": "The conditioning output of a FLUX Fill invocation.", + "properties": { + "fill_cond": { + "$ref": "#/components/schemas/FluxFillConditioningField", + "description": "FLUX Redux conditioning tensor", + "field_kind": "output", + "title": "Conditioning", + "ui_hidden": false + }, + "type": { + "const": "flux_fill_output", + "default": "flux_fill_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "fill_cond", "type", "type"], + "title": "FluxFillOutput", + "type": "object" + }, + "FluxIPAdapterInvocation": { + "category": "conditioning", + "class": "invocation", + "classification": "stable", + "description": "Collects FLUX IP-Adapter info to pass to other nodes.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The IP-Adapter image prompt(s).", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "ip_adapter_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The IP-Adapter model.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "IP-Adapter Model", + "ui_model_base": ["flux"], + "ui_model_type": ["ip_adapter"] + }, + "clip_vision_model": { + "const": "ViT-L", + "default": "ViT-L", + "description": "CLIP Vision model to use.", + "field_kind": "input", + "input": "any", + "orig_default": "ViT-L", + "orig_required": false, + "title": "Clip Vision Model", + "type": "string" + }, + "weight": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 1, + "description": "The weight given to the IP-Adapter", + "field_kind": "input", + "input": "any", + "orig_default": 1, + "orig_required": false, + "title": "Weight" + }, + "begin_step_percent": { + "default": 0, + "description": "When the IP-Adapter is first applied (% of total steps)", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Begin Step Percent", + "type": "number" + }, + "end_step_percent": { + "default": 1, + "description": "When the IP-Adapter is last applied (% of total steps)", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1, + "orig_required": false, + "title": "End Step Percent", + "type": "number" + }, + "type": { + "const": "flux_ip_adapter", + "default": "flux_ip_adapter", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["ip_adapter", "control"], + "title": "FLUX IP-Adapter", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/IPAdapterOutput" + } + }, + "FluxKontextConcatenateImagesInvocation": { + "category": "conditioning", + "class": "invocation", + "classification": "stable", + "description": "Prepares an image or images for use with FLUX Kontext. The first/single image is resized to the nearest\npreferred Kontext resolution. All other images are concatenated horizontally, maintaining their aspect ratio.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "images": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ImageField" + }, + "maxItems": 10, + "minItems": 1, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The images to concatenate", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Images" + }, + "use_preferred_resolution": { + "default": true, + "description": "Use FLUX preferred resolutions for the first image", + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "Use Preferred Resolution", + "type": "boolean" + }, + "type": { + "const": "flux_kontext_image_prep", + "default": "flux_kontext_image_prep", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "concatenate", "flux", "kontext"], + "title": "FLUX Kontext Image Prep", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "FluxKontextConditioningField": { + "description": "A conditioning field for FLUX Kontext (reference image).", + "properties": { + "image": { + "$ref": "#/components/schemas/ImageField", + "description": "The Kontext reference image." + } + }, + "required": ["image"], + "title": "FluxKontextConditioningField", + "type": "object" + }, + "FluxKontextInvocation": { + "category": "conditioning", + "class": "invocation", + "classification": "stable", + "description": "Prepares a reference image for FLUX Kontext conditioning.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The Kontext reference image.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "flux_kontext", + "default": "flux_kontext", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["conditioning", "kontext", "flux"], + "title": "Kontext Conditioning - FLUX", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/FluxKontextOutput" + } + }, + "FluxKontextOutput": { + "class": "output", + "description": "The conditioning output of a FLUX Kontext invocation.", + "properties": { + "kontext_cond": { + "$ref": "#/components/schemas/FluxKontextConditioningField", + "description": "FLUX Kontext conditioning (reference image)", + "field_kind": "output", + "title": "Kontext Conditioning", + "ui_hidden": false + }, + "type": { + "const": "flux_kontext_output", + "default": "flux_kontext_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "kontext_cond", "type", "type"], + "title": "FluxKontextOutput", + "type": "object" + }, + "FluxLoRALoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Apply a LoRA model to a FLUX transformer and/or text encoder.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "lora": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LoRA model to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "LoRA", + "ui_model_base": ["flux"], + "ui_model_type": ["lora"] + }, + "weight": { + "default": 0.75, + "description": "The weight at which the LoRA is applied to each model", + "field_kind": "input", + "input": "any", + "orig_default": 0.75, + "orig_required": false, + "title": "Weight", + "type": "number" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Transformer", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "FLUX Transformer" + }, + "clip": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "CLIP" + }, + "t5_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/T5EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "T5 tokenizer and text encoder", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "T5 Encoder" + }, + "type": { + "const": "flux_lora_loader", + "default": "flux_lora_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["lora", "model", "flux"], + "title": "Apply LoRA - FLUX", + "type": "object", + "version": "1.2.1", + "output": { + "$ref": "#/components/schemas/FluxLoRALoaderOutput" + } + }, + "FluxLoRALoaderOutput": { + "class": "output", + "description": "FLUX LoRA Loader Output", + "properties": { + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Transformer", + "field_kind": "output", + "title": "FLUX Transformer", + "ui_hidden": false + }, + "clip": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "output", + "title": "CLIP", + "ui_hidden": false + }, + "t5_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/T5EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "T5 tokenizer and text encoder", + "field_kind": "output", + "title": "T5 Encoder", + "ui_hidden": false + }, + "type": { + "const": "flux_lora_loader_output", + "default": "flux_lora_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "transformer", "clip", "t5_encoder", "type", "type"], + "title": "FluxLoRALoaderOutput", + "type": "object" + }, + "FluxModelLoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Loads a flux base model, outputting its submodels.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Flux model (Transformer) to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "ui_model_base": ["flux"], + "ui_model_type": ["main"] + }, + "t5_encoder_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "T5 tokenizer and text encoder", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "T5 Encoder", + "ui_model_type": ["t5_encoder"] + }, + "clip_embed_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP Embed loader", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "CLIP Embed", + "ui_model_type": ["clip_embed"] + }, + "vae_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE model to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "VAE", + "ui_model_base": ["flux"], + "ui_model_type": ["vae"] + }, + "type": { + "const": "flux_model_loader", + "default": "flux_model_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["model", "flux"], + "title": "Main Model - FLUX", + "type": "object", + "version": "1.0.7", + "output": { + "$ref": "#/components/schemas/FluxModelLoaderOutput" + } + }, + "FluxModelLoaderOutput": { + "class": "output", + "description": "Flux base model loader output", + "properties": { + "transformer": { + "$ref": "#/components/schemas/TransformerField", + "description": "Transformer", + "field_kind": "output", + "title": "Transformer", + "ui_hidden": false + }, + "clip": { + "$ref": "#/components/schemas/CLIPField", + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "output", + "title": "CLIP", + "ui_hidden": false + }, + "t5_encoder": { + "$ref": "#/components/schemas/T5EncoderField", + "description": "T5 tokenizer and text encoder", + "field_kind": "output", + "title": "T5 Encoder", + "ui_hidden": false + }, + "vae": { + "$ref": "#/components/schemas/VAEField", + "description": "VAE", + "field_kind": "output", + "title": "VAE", + "ui_hidden": false + }, + "max_seq_len": { + "description": "The max sequence length to used for the T5 encoder. (256 for schnell transformer, 512 for dev transformer)", + "enum": [256, 512], + "field_kind": "output", + "title": "Max Seq Length", + "type": "integer", + "ui_hidden": false + }, + "type": { + "const": "flux_model_loader_output", + "default": "flux_model_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "transformer", "clip", "t5_encoder", "vae", "max_seq_len", "type", "type"], + "title": "FluxModelLoaderOutput", + "type": "object" + }, + "FluxReduxConditioningField": { + "description": "A FLUX Redux conditioning tensor primitive value", + "properties": { + "conditioning": { + "$ref": "#/components/schemas/TensorField", + "description": "The Redux image conditioning tensor." + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The mask associated with this conditioning tensor. Excluded regions should be set to False, included regions should be set to True." + } + }, + "required": ["conditioning"], + "title": "FluxReduxConditioningField", + "type": "object" + }, + "FluxReduxInvocation": { + "category": "conditioning", + "class": "invocation", + "classification": "beta", + "description": "Runs a FLUX Redux model to generate a conditioning tensor.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The FLUX Redux image prompt.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "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.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "redux_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The FLUX Redux model to use.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "FLUX Redux Model", + "ui_model_base": ["flux"], + "ui_model_type": ["flux_redux"] + }, + "downsampling_factor": { + "default": 1, + "description": "Redux Downsampling Factor (1-9)", + "field_kind": "input", + "input": "any", + "maximum": 9, + "minimum": 1, + "orig_default": 1, + "orig_required": false, + "title": "Downsampling Factor", + "type": "integer" + }, + "downsampling_function": { + "default": "area", + "description": "Redux Downsampling Function", + "enum": ["nearest", "bilinear", "bicubic", "area", "nearest-exact"], + "field_kind": "input", + "input": "any", + "orig_default": "area", + "orig_required": false, + "title": "Downsampling Function", + "type": "string" + }, + "weight": { + "default": 1.0, + "description": "Redux weight (0.0-1.0)", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Weight", + "type": "number" + }, + "type": { + "const": "flux_redux", + "default": "flux_redux", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["ip_adapter", "control"], + "title": "FLUX Redux", + "type": "object", + "version": "2.1.0", + "output": { + "$ref": "#/components/schemas/FluxReduxOutput" + } + }, + "FluxReduxOutput": { + "class": "output", + "description": "The conditioning output of a FLUX Redux invocation.", + "properties": { + "redux_cond": { + "$ref": "#/components/schemas/FluxReduxConditioningField", + "description": "FLUX Redux conditioning tensor", + "field_kind": "output", + "title": "Conditioning", + "ui_hidden": false + }, + "type": { + "const": "flux_redux_output", + "default": "flux_redux_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "redux_cond", "type", "type"], + "title": "FluxReduxOutput", + "type": "object" + }, + "FluxTextEncoderInvocation": { + "category": "prompt", + "class": "invocation", + "classification": "stable", + "description": "Encodes and preps a prompt for a flux image.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "clip": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "CLIP" + }, + "t5_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/T5EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "T5 tokenizer and text encoder", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "T5Encoder" + }, + "t5_max_seq_len": { + "anyOf": [ + { + "enum": [256, 512], + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Max sequence length for the T5 encoder. Expected to be 256 for FLUX schnell models and 512 for FLUX dev models.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "T5 Max Seq Len" + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Text prompt to encode.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Prompt", + "ui_component": "textarea" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A mask defining the region that this conditioning prompt applies to.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "type": { + "const": "flux_text_encoder", + "default": "flux_text_encoder", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["prompt", "conditioning", "flux"], + "title": "Prompt - FLUX", + "type": "object", + "version": "1.1.2", + "output": { + "$ref": "#/components/schemas/FluxConditioningOutput" + } + }, + "FluxVaeDecodeInvocation": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Generates an image from latents.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "type": { + "const": "flux_vae_decode", + "default": "flux_vae_decode", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "image", "vae", "l2i", "flux"], + "title": "Latents to Image - FLUX", + "type": "object", + "version": "1.0.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "FluxVaeEncodeInvocation": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Encodes an image into latents.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to encode.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "type": { + "const": "flux_vae_encode", + "default": "flux_vae_encode", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "image", "vae", "i2l", "flux"], + "title": "Image to Latents - FLUX", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "FluxVariantType": { + "type": "string", + "enum": ["schnell", "dev", "dev_fill"], + "title": "FluxVariantType", + "description": "FLUX.1 model variants." + }, + "FoundModel": { + "properties": { + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model" + }, + "is_installed": { + "type": "boolean", + "title": "Is Installed", + "description": "Whether or not the model is already installed" + } + }, + "type": "object", + "required": ["path", "is_installed"], + "title": "FoundModel" + }, + "FreeUConfig": { + "description": "Configuration for the FreeU hyperparameters.\n- https://huggingface.co/docs/diffusers/main/en/using-diffusers/freeu\n- https://github.com/ChenyangSi/FreeU", + "properties": { + "s1": { + "description": "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.", + "maximum": 3, + "minimum": -1, + "title": "S1", + "type": "number" + }, + "s2": { + "description": "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.", + "maximum": 3, + "minimum": -1, + "title": "S2", + "type": "number" + }, + "b1": { + "description": "Scaling factor for stage 1 to amplify the contributions of backbone features.", + "maximum": 3, + "minimum": -1, + "title": "B1", + "type": "number" + }, + "b2": { + "description": "Scaling factor for stage 2 to amplify the contributions of backbone features.", + "maximum": 3, + "minimum": -1, + "title": "B2", + "type": "number" + } + }, + "required": ["s1", "s2", "b1", "b2"], + "title": "FreeUConfig", + "type": "object" + }, + "FreeUInvocation": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Applies FreeU to the UNet. Suggested values (b1/b2/s1/s2):\n\nSD1.5: 1.2/1.4/0.9/0.2,\nSD2: 1.1/1.2/0.9/0.2,\nSDXL: 1.1/1.2/0.6/0.4,", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "unet": { + "anyOf": [ + { + "$ref": "#/components/schemas/UNetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "UNet (scheduler, LoRAs)", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "UNet" + }, + "b1": { + "default": 1.2, + "description": "Scaling factor for stage 1 to amplify the contributions of backbone features.", + "field_kind": "input", + "input": "any", + "maximum": 3, + "minimum": -1, + "orig_default": 1.2, + "orig_required": false, + "title": "B1", + "type": "number" + }, + "b2": { + "default": 1.4, + "description": "Scaling factor for stage 2 to amplify the contributions of backbone features.", + "field_kind": "input", + "input": "any", + "maximum": 3, + "minimum": -1, + "orig_default": 1.4, + "orig_required": false, + "title": "B2", + "type": "number" + }, + "s1": { + "default": 0.9, + "description": "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.", + "field_kind": "input", + "input": "any", + "maximum": 3, + "minimum": -1, + "orig_default": 0.9, + "orig_required": false, + "title": "S1", + "type": "number" + }, + "s2": { + "default": 0.2, + "description": "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.", + "field_kind": "input", + "input": "any", + "maximum": 3, + "minimum": -1, + "orig_default": 0.2, + "orig_required": false, + "title": "S2", + "type": "number" + }, + "type": { + "const": "freeu", + "default": "freeu", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["freeu"], + "title": "Apply FreeU - SD1.5, SDXL", + "type": "object", + "version": "1.0.2", + "output": { + "$ref": "#/components/schemas/UNetOutput" + } + }, + "GeminiImageGenerationInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Generate images using a Gemini-hosted external model.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Main model (UNet, VAE, CLIP) to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "ui_model_base": ["external"], + "ui_model_format": ["external_api"], + "ui_model_provider_id": ["gemini"], + "ui_model_type": ["external_image_generator"] + }, + "mode": { + "default": "txt2img", + "description": "Generation mode.", + "enum": ["txt2img", "img2img", "inpaint"], + "field_kind": "input", + "input": "any", + "orig_default": "txt2img", + "orig_required": false, + "title": "Mode", + "type": "string", + "ui_hidden": true + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Prompt", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Prompt" + }, + "seed": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Seed for random number generation", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Seed" + }, + "num_images": { + "default": 1, + "description": "Number of images to generate", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 1, + "orig_required": false, + "title": "Num Images", + "type": "integer" + }, + "width": { + "default": 1024, + "description": "Width of output (px)", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 1024, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 1024, + "description": "Height of output (px)", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 1024, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "image_size": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Image size preset (e.g. 1K, 2K, 4K)", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Image Size" + }, + "init_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Init image for img2img/inpaint", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "ui_hidden": true + }, + "mask_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Mask image for inpaint", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "ui_hidden": true + }, + "reference_images": { + "default": [], + "description": "Reference images", + "field_kind": "input", + "input": "any", + "items": { + "$ref": "#/components/schemas/ImageField" + }, + "orig_default": [], + "orig_required": false, + "title": "Reference Images", + "type": "array" + }, + "temperature": { + "anyOf": [ + { + "maximum": 2.0, + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Sampling temperature", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Temperature" + }, + "thinking_level": { + "anyOf": [ + { + "enum": ["minimal", "high"], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Thinking level for image generation", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Thinking Level" + }, + "type": { + "const": "gemini_image_generation", + "default": "gemini_image_generation", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["external", "generation", "gemini"], + "title": "Gemini Image Generation", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageCollectionOutput" + } + }, + "GeneratePasswordResponse": { + "properties": { + "password": { + "type": "string", + "title": "Password", + "description": "Generated strong password" + } + }, + "type": "object", + "required": ["password"], + "title": "GeneratePasswordResponse", + "description": "Response containing a generated password." + }, + "GetMaskBoundingBoxInvocation": { + "category": "mask", + "class": "invocation", + "classification": "stable", + "description": "Gets the bounding box of the given mask image.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The mask to crop.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "margin": { + "default": 0, + "description": "Margin to add to the bounding box.", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Margin", + "type": "integer" + }, + "mask_color": { + "$ref": "#/components/schemas/ColorField", + "default": { + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "description": "Color of the mask in the image.", + "field_kind": "input", + "input": "any", + "orig_default": { + "a": 255, + "b": 255, + "g": 255, + "r": 255 + }, + "orig_required": false + }, + "type": { + "const": "get_image_mask_bounding_box", + "default": "get_image_mask_bounding_box", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["mask"], + "title": "Get Image Mask Bounding Box", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/BoundingBoxOutput" + } + }, + "GlmEncoderField": { + "properties": { + "tokenizer": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Info to load tokenizer submodel" + }, + "text_encoder": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Info to load text_encoder submodel" + } + }, + "required": ["tokenizer", "text_encoder"], + "title": "GlmEncoderField", + "type": "object" + }, + "GradientMaskOutput": { + "class": "output", + "description": "Outputs a denoise mask and an image representing the total gradient of the mask.", + "properties": { + "denoise_mask": { + "$ref": "#/components/schemas/DenoiseMaskField", + "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.", + "field_kind": "output", + "ui_hidden": false + }, + "expanded_mask_area": { + "$ref": "#/components/schemas/ImageField", + "description": "Image representing the total gradient area of the mask. For paste-back purposes.", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "gradient_mask_output", + "default": "gradient_mask_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "denoise_mask", "expanded_mask_area", "type", "type"], + "title": "GradientMaskOutput", + "type": "object" + }, + "Graph": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "The id of this graph" + }, + "nodes": { + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/components/schemas/AddInvocation" + }, + { + "$ref": "#/components/schemas/AlibabaCloudImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/AlphaMaskToTensorInvocation" + }, + { + "$ref": "#/components/schemas/AnimaDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/AnimaImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/AnimaLatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/AnimaLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/AnimaLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/AnimaModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/AnimaTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/ApplyMaskTensorToImageInvocation" + }, + { + "$ref": "#/components/schemas/ApplyMaskToImageInvocation" + }, + { + "$ref": "#/components/schemas/BlankImageInvocation" + }, + { + "$ref": "#/components/schemas/BlendLatentsInvocation" + }, + { + "$ref": "#/components/schemas/BooleanCollectionInvocation" + }, + { + "$ref": "#/components/schemas/BooleanInvocation" + }, + { + "$ref": "#/components/schemas/BoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/CLIPSkipInvocation" + }, + { + "$ref": "#/components/schemas/CV2InfillInvocation" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesEvenSplitInvocation" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesInvocation" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesMinimumOverlapInvocation" + }, + { + "$ref": "#/components/schemas/CannyEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/CanvasOutputInvocation" + }, + { + "$ref": "#/components/schemas/CanvasPasteBackInvocation" + }, + { + "$ref": "#/components/schemas/CanvasV2MaskAndCropInvocation" + }, + { + "$ref": "#/components/schemas/CenterPadCropInvocation" + }, + { + "$ref": "#/components/schemas/CogView4DenoiseInvocation" + }, + { + "$ref": "#/components/schemas/CogView4ImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/CogView4LatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/CogView4ModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/CogView4TextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/CollectInvocation" + }, + { + "$ref": "#/components/schemas/ColorCorrectInvocation" + }, + { + "$ref": "#/components/schemas/ColorInvocation" + }, + { + "$ref": "#/components/schemas/ColorMapInvocation" + }, + { + "$ref": "#/components/schemas/CompelInvocation" + }, + { + "$ref": "#/components/schemas/ConditioningCollectionInvocation" + }, + { + "$ref": "#/components/schemas/ConditioningInvocation" + }, + { + "$ref": "#/components/schemas/ContentShuffleInvocation" + }, + { + "$ref": "#/components/schemas/ControlNetInvocation" + }, + { + "$ref": "#/components/schemas/CoreMetadataInvocation" + }, + { + "$ref": "#/components/schemas/CreateDenoiseMaskInvocation" + }, + { + "$ref": "#/components/schemas/CreateGradientMaskInvocation" + }, + { + "$ref": "#/components/schemas/CropImageToBoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/CropLatentsCoreInvocation" + }, + { + "$ref": "#/components/schemas/CvInpaintInvocation" + }, + { + "$ref": "#/components/schemas/DWOpenposeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/DecodeInvisibleWatermarkInvocation" + }, + { + "$ref": "#/components/schemas/DenoiseLatentsInvocation" + }, + { + "$ref": "#/components/schemas/DenoiseLatentsMetaInvocation" + }, + { + "$ref": "#/components/schemas/DepthAnythingDepthEstimationInvocation" + }, + { + "$ref": "#/components/schemas/DivideInvocation" + }, + { + "$ref": "#/components/schemas/DynamicPromptInvocation" + }, + { + "$ref": "#/components/schemas/ESRGANInvocation" + }, + { + "$ref": "#/components/schemas/ExpandMaskWithFadeInvocation" + }, + { + "$ref": "#/components/schemas/FLUXLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/FaceIdentifierInvocation" + }, + { + "$ref": "#/components/schemas/FaceMaskInvocation" + }, + { + "$ref": "#/components/schemas/FaceOffInvocation" + }, + { + "$ref": "#/components/schemas/FloatBatchInvocation" + }, + { + "$ref": "#/components/schemas/FloatCollectionInvocation" + }, + { + "$ref": "#/components/schemas/FloatGenerator" + }, + { + "$ref": "#/components/schemas/FloatInvocation" + }, + { + "$ref": "#/components/schemas/FloatLinearRangeInvocation" + }, + { + "$ref": "#/components/schemas/FloatMathInvocation" + }, + { + "$ref": "#/components/schemas/FloatToIntegerInvocation" + }, + { + "$ref": "#/components/schemas/Flux2DenoiseInvocation" + }, + { + "$ref": "#/components/schemas/Flux2KleinLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/Flux2KleinLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/Flux2KleinModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/Flux2KleinTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/Flux2VaeDecodeInvocation" + }, + { + "$ref": "#/components/schemas/Flux2VaeEncodeInvocation" + }, + { + "$ref": "#/components/schemas/FluxControlLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/FluxControlNetInvocation" + }, + { + "$ref": "#/components/schemas/FluxDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/FluxDenoiseLatentsMetaInvocation" + }, + { + "$ref": "#/components/schemas/FluxFillInvocation" + }, + { + "$ref": "#/components/schemas/FluxIPAdapterInvocation" + }, + { + "$ref": "#/components/schemas/FluxKontextConcatenateImagesInvocation" + }, + { + "$ref": "#/components/schemas/FluxKontextInvocation" + }, + { + "$ref": "#/components/schemas/FluxLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/FluxModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/FluxReduxInvocation" + }, + { + "$ref": "#/components/schemas/FluxTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/FluxVaeDecodeInvocation" + }, + { + "$ref": "#/components/schemas/FluxVaeEncodeInvocation" + }, + { + "$ref": "#/components/schemas/FreeUInvocation" + }, + { + "$ref": "#/components/schemas/GeminiImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/GetMaskBoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/GroundingDinoInvocation" + }, + { + "$ref": "#/components/schemas/HEDEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/HeuristicResizeInvocation" + }, + { + "$ref": "#/components/schemas/IPAdapterInvocation" + }, + { + "$ref": "#/components/schemas/IdealSizeInvocation" + }, + { + "$ref": "#/components/schemas/IfInvocation" + }, + { + "$ref": "#/components/schemas/ImageBatchInvocation" + }, + { + "$ref": "#/components/schemas/ImageBlurInvocation" + }, + { + "$ref": "#/components/schemas/ImageChannelInvocation" + }, + { + "$ref": "#/components/schemas/ImageChannelMultiplyInvocation" + }, + { + "$ref": "#/components/schemas/ImageChannelOffsetInvocation" + }, + { + "$ref": "#/components/schemas/ImageCollectionInvocation" + }, + { + "$ref": "#/components/schemas/ImageConvertInvocation" + }, + { + "$ref": "#/components/schemas/ImageCropInvocation" + }, + { + "$ref": "#/components/schemas/ImageGenerator" + }, + { + "$ref": "#/components/schemas/ImageHueAdjustmentInvocation" + }, + { + "$ref": "#/components/schemas/ImageInverseLerpInvocation" + }, + { + "$ref": "#/components/schemas/ImageInvocation" + }, + { + "$ref": "#/components/schemas/ImageLerpInvocation" + }, + { + "$ref": "#/components/schemas/ImageMaskToTensorInvocation" + }, + { + "$ref": "#/components/schemas/ImageMultiplyInvocation" + }, + { + "$ref": "#/components/schemas/ImageNSFWBlurInvocation" + }, + { + "$ref": "#/components/schemas/ImageNoiseInvocation" + }, + { + "$ref": "#/components/schemas/ImagePanelLayoutInvocation" + }, + { + "$ref": "#/components/schemas/ImagePasteInvocation" + }, + { + "$ref": "#/components/schemas/ImageResizeInvocation" + }, + { + "$ref": "#/components/schemas/ImageScaleInvocation" + }, + { + "$ref": "#/components/schemas/ImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/ImageWatermarkInvocation" + }, + { + "$ref": "#/components/schemas/InfillColorInvocation" + }, + { + "$ref": "#/components/schemas/InfillPatchMatchInvocation" + }, + { + "$ref": "#/components/schemas/InfillTileInvocation" + }, + { + "$ref": "#/components/schemas/IntegerBatchInvocation" + }, + { + "$ref": "#/components/schemas/IntegerCollectionInvocation" + }, + { + "$ref": "#/components/schemas/IntegerGenerator" + }, + { + "$ref": "#/components/schemas/IntegerInvocation" + }, + { + "$ref": "#/components/schemas/IntegerMathInvocation" + }, + { + "$ref": "#/components/schemas/InvertTensorMaskInvocation" + }, + { + "$ref": "#/components/schemas/InvokeAdjustImageHuePlusInvocation" + }, + { + "$ref": "#/components/schemas/InvokeEquivalentAchromaticLightnessInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageBlendInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageCompositorInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageDilateOrErodeInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageEnhanceInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageValueThresholdsInvocation" + }, + { + "$ref": "#/components/schemas/IterateInvocation" + }, + { + "$ref": "#/components/schemas/LaMaInfillInvocation" + }, + { + "$ref": "#/components/schemas/LatentsCollectionInvocation" + }, + { + "$ref": "#/components/schemas/LatentsInvocation" + }, + { + "$ref": "#/components/schemas/LatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/LineartAnimeEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/LineartEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/LlavaOnevisionVllmInvocation" + }, + { + "$ref": "#/components/schemas/LoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/LoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/LoRASelectorInvocation" + }, + { + "$ref": "#/components/schemas/MLSDDetectionInvocation" + }, + { + "$ref": "#/components/schemas/MainModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/MaskCombineInvocation" + }, + { + "$ref": "#/components/schemas/MaskEdgeInvocation" + }, + { + "$ref": "#/components/schemas/MaskFromAlphaInvocation" + }, + { + "$ref": "#/components/schemas/MaskFromIDInvocation" + }, + { + "$ref": "#/components/schemas/MaskTensorToImageInvocation" + }, + { + "$ref": "#/components/schemas/MediaPipeFaceDetectionInvocation" + }, + { + "$ref": "#/components/schemas/MergeMetadataInvocation" + }, + { + "$ref": "#/components/schemas/MergeTilesToImageInvocation" + }, + { + "$ref": "#/components/schemas/MetadataFieldExtractorInvocation" + }, + { + "$ref": "#/components/schemas/MetadataFromImageInvocation" + }, + { + "$ref": "#/components/schemas/MetadataInvocation" + }, + { + "$ref": "#/components/schemas/MetadataItemInvocation" + }, + { + "$ref": "#/components/schemas/MetadataItemLinkedInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToBoolCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToBoolInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToControlnetsInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToFloatCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToFloatInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToIPAdaptersInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToIntegerCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToIntegerInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToLorasCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToLorasInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToModelInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToSDXLLorasInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToSDXLModelInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToSchedulerInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToStringCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToStringInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToT2IAdaptersInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToVAEInvocation" + }, + { + "$ref": "#/components/schemas/ModelIdentifierInvocation" + }, + { + "$ref": "#/components/schemas/MultiplyInvocation" + }, + { + "$ref": "#/components/schemas/NoiseInvocation" + }, + { + "$ref": "#/components/schemas/NormalMapInvocation" + }, + { + "$ref": "#/components/schemas/OklabUnsharpMaskInvocation" + }, + { + "$ref": "#/components/schemas/OklchImageHueAdjustmentInvocation" + }, + { + "$ref": "#/components/schemas/OpenAIImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/PBRMapsInvocation" + }, + { + "$ref": "#/components/schemas/PairTileImageInvocation" + }, + { + "$ref": "#/components/schemas/PasteImageIntoBoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/PiDiNetEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/PromptTemplateInvocation" + }, + { + "$ref": "#/components/schemas/PromptsFromFileInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageLatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/QwenImageLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/RandomFloatInvocation" + }, + { + "$ref": "#/components/schemas/RandomIntInvocation" + }, + { + "$ref": "#/components/schemas/RandomRangeInvocation" + }, + { + "$ref": "#/components/schemas/RangeInvocation" + }, + { + "$ref": "#/components/schemas/RangeOfSizeInvocation" + }, + { + "$ref": "#/components/schemas/RectangleMaskInvocation" + }, + { + "$ref": "#/components/schemas/ResizeLatentsInvocation" + }, + { + "$ref": "#/components/schemas/RoundInvocation" + }, + { + "$ref": "#/components/schemas/SD3DenoiseInvocation" + }, + { + "$ref": "#/components/schemas/SD3ImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/SD3LatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/SDXLCompelPromptInvocation" + }, + { + "$ref": "#/components/schemas/SDXLLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/SDXLLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/SDXLModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/SDXLRefinerCompelPromptInvocation" + }, + { + "$ref": "#/components/schemas/SDXLRefinerModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/SaveImageInvocation" + }, + { + "$ref": "#/components/schemas/SaveImageToFileInvocation" + }, + { + "$ref": "#/components/schemas/ScaleLatentsInvocation" + }, + { + "$ref": "#/components/schemas/SchedulerInvocation" + }, + { + "$ref": "#/components/schemas/Sd3ModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/Sd3TextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/SeamlessModeInvocation" + }, + { + "$ref": "#/components/schemas/SeedreamImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/SegmentAnythingInvocation" + }, + { + "$ref": "#/components/schemas/ShowImageInvocation" + }, + { + "$ref": "#/components/schemas/SpandrelImageToImageAutoscaleInvocation" + }, + { + "$ref": "#/components/schemas/SpandrelImageToImageInvocation" + }, + { + "$ref": "#/components/schemas/StringBatchInvocation" + }, + { + "$ref": "#/components/schemas/StringCollectionInvocation" + }, + { + "$ref": "#/components/schemas/StringGenerator" + }, + { + "$ref": "#/components/schemas/StringInvocation" + }, + { + "$ref": "#/components/schemas/StringJoinInvocation" + }, + { + "$ref": "#/components/schemas/StringJoinThreeInvocation" + }, + { + "$ref": "#/components/schemas/StringReplaceInvocation" + }, + { + "$ref": "#/components/schemas/StringSplitInvocation" + }, + { + "$ref": "#/components/schemas/StringSplitNegInvocation" + }, + { + "$ref": "#/components/schemas/SubtractInvocation" + }, + { + "$ref": "#/components/schemas/T2IAdapterInvocation" + }, + { + "$ref": "#/components/schemas/TextLLMInvocation" + }, + { + "$ref": "#/components/schemas/TileToPropertiesInvocation" + }, + { + "$ref": "#/components/schemas/TiledMultiDiffusionDenoiseLatents" + }, + { + "$ref": "#/components/schemas/UnsharpMaskInvocation" + }, + { + "$ref": "#/components/schemas/VAELoaderInvocation" + }, + { + "$ref": "#/components/schemas/ZImageControlInvocation" + }, + { + "$ref": "#/components/schemas/ZImageDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/ZImageDenoiseMetaInvocation" + }, + { + "$ref": "#/components/schemas/ZImageImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/ZImageLatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/ZImageLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/ZImageLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/ZImageModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/ZImageSeedVarianceEnhancerInvocation" + }, + { + "$ref": "#/components/schemas/ZImageTextEncoderInvocation" + } + ] + }, + "type": "object", + "title": "Nodes", + "description": "The nodes in this graph" + }, + "edges": { + "items": { + "$ref": "#/components/schemas/Edge" + }, + "type": "array", + "title": "Edges", + "description": "The connections between nodes and their fields in this graph" + } + }, + "type": "object", + "title": "Graph", + "description": "A validated invocation graph made of nodes and typed edges." + }, + "GraphExecutionState": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "The id of the execution state" + }, + "graph": { + "$ref": "#/components/schemas/Graph", + "description": "The graph being executed" + }, + "execution_graph": { + "$ref": "#/components/schemas/Graph", + "description": "The expanded graph of activated and executed nodes" + }, + "executed": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true, + "title": "Executed", + "description": "The set of node ids that have been executed" + }, + "executed_history": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Executed History", + "description": "The list of node ids that have been executed, in order of execution" + }, + "results": { + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/components/schemas/AnimaConditioningOutput" + }, + { + "$ref": "#/components/schemas/AnimaLoRALoaderOutput" + }, + { + "$ref": "#/components/schemas/AnimaModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/BooleanCollectionOutput" + }, + { + "$ref": "#/components/schemas/BooleanOutput" + }, + { + "$ref": "#/components/schemas/BoundingBoxCollectionOutput" + }, + { + "$ref": "#/components/schemas/BoundingBoxOutput" + }, + { + "$ref": "#/components/schemas/CLIPOutput" + }, + { + "$ref": "#/components/schemas/CLIPSkipInvocationOutput" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesOutput" + }, + { + "$ref": "#/components/schemas/CogView4ConditioningOutput" + }, + { + "$ref": "#/components/schemas/CogView4ModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/CollectInvocationOutput" + }, + { + "$ref": "#/components/schemas/ColorCollectionOutput" + }, + { + "$ref": "#/components/schemas/ColorOutput" + }, + { + "$ref": "#/components/schemas/ConditioningCollectionOutput" + }, + { + "$ref": "#/components/schemas/ConditioningOutput" + }, + { + "$ref": "#/components/schemas/ControlOutput" + }, + { + "$ref": "#/components/schemas/DenoiseMaskOutput" + }, + { + "$ref": "#/components/schemas/FaceMaskOutput" + }, + { + "$ref": "#/components/schemas/FaceOffOutput" + }, + { + "$ref": "#/components/schemas/FloatCollectionOutput" + }, + { + "$ref": "#/components/schemas/FloatGeneratorOutput" + }, + { + "$ref": "#/components/schemas/FloatOutput" + }, + { + "$ref": "#/components/schemas/Flux2KleinLoRALoaderOutput" + }, + { + "$ref": "#/components/schemas/Flux2KleinModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/FluxConditioningCollectionOutput" + }, + { + "$ref": "#/components/schemas/FluxConditioningOutput" + }, + { + "$ref": "#/components/schemas/FluxControlLoRALoaderOutput" + }, + { + "$ref": "#/components/schemas/FluxControlNetOutput" + }, + { + "$ref": "#/components/schemas/FluxFillOutput" + }, + { + "$ref": "#/components/schemas/FluxKontextOutput" + }, + { + "$ref": "#/components/schemas/FluxLoRALoaderOutput" + }, + { + "$ref": "#/components/schemas/FluxModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/FluxReduxOutput" + }, + { + "$ref": "#/components/schemas/GradientMaskOutput" + }, + { + "$ref": "#/components/schemas/IPAdapterOutput" + }, + { + "$ref": "#/components/schemas/IdealSizeOutput" + }, + { + "$ref": "#/components/schemas/IfInvocationOutput" + }, + { + "$ref": "#/components/schemas/ImageCollectionOutput" + }, + { + "$ref": "#/components/schemas/ImageGeneratorOutput" + }, + { + "$ref": "#/components/schemas/ImageOutput" + }, + { + "$ref": "#/components/schemas/ImagePanelCoordinateOutput" + }, + { + "$ref": "#/components/schemas/IntegerCollectionOutput" + }, + { + "$ref": "#/components/schemas/IntegerGeneratorOutput" + }, + { + "$ref": "#/components/schemas/IntegerOutput" + }, + { + "$ref": "#/components/schemas/IterateInvocationOutput" + }, + { + "$ref": "#/components/schemas/LatentsCollectionOutput" + }, + { + "$ref": "#/components/schemas/LatentsMetaOutput" + }, + { + "$ref": "#/components/schemas/LatentsOutput" + }, + { + "$ref": "#/components/schemas/LoRALoaderOutput" + }, + { + "$ref": "#/components/schemas/LoRASelectorOutput" + }, + { + "$ref": "#/components/schemas/MDControlListOutput" + }, + { + "$ref": "#/components/schemas/MDIPAdapterListOutput" + }, + { + "$ref": "#/components/schemas/MDT2IAdapterListOutput" + }, + { + "$ref": "#/components/schemas/MaskOutput" + }, + { + "$ref": "#/components/schemas/MetadataItemOutput" + }, + { + "$ref": "#/components/schemas/MetadataOutput" + }, + { + "$ref": "#/components/schemas/MetadataToLorasCollectionOutput" + }, + { + "$ref": "#/components/schemas/MetadataToModelOutput" + }, + { + "$ref": "#/components/schemas/MetadataToSDXLModelOutput" + }, + { + "$ref": "#/components/schemas/ModelIdentifierOutput" + }, + { + "$ref": "#/components/schemas/ModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/NoiseOutput" + }, + { + "$ref": "#/components/schemas/PBRMapsOutput" + }, + { + "$ref": "#/components/schemas/PairTileImageOutput" + }, + { + "$ref": "#/components/schemas/PromptTemplateOutput" + }, + { + "$ref": "#/components/schemas/QwenImageConditioningOutput" + }, + { + "$ref": "#/components/schemas/QwenImageLoRALoaderOutput" + }, + { + "$ref": "#/components/schemas/QwenImageModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/SD3ConditioningOutput" + }, + { + "$ref": "#/components/schemas/SDXLLoRALoaderOutput" + }, + { + "$ref": "#/components/schemas/SDXLModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/SDXLRefinerModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/SchedulerOutput" + }, + { + "$ref": "#/components/schemas/Sd3ModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/SeamlessModeOutput" + }, + { + "$ref": "#/components/schemas/String2Output" + }, + { + "$ref": "#/components/schemas/StringCollectionOutput" + }, + { + "$ref": "#/components/schemas/StringGeneratorOutput" + }, + { + "$ref": "#/components/schemas/StringOutput" + }, + { + "$ref": "#/components/schemas/StringPosNegOutput" + }, + { + "$ref": "#/components/schemas/T2IAdapterOutput" + }, + { + "$ref": "#/components/schemas/TileToPropertiesOutput" + }, + { + "$ref": "#/components/schemas/UNetOutput" + }, + { + "$ref": "#/components/schemas/VAEOutput" + }, + { + "$ref": "#/components/schemas/ZImageConditioningOutput" + }, + { + "$ref": "#/components/schemas/ZImageControlOutput" + }, + { + "$ref": "#/components/schemas/ZImageLoRALoaderOutput" + }, + { + "$ref": "#/components/schemas/ZImageModelLoaderOutput" + } + ] + }, + "type": "object", + "title": "Results", + "description": "The results of node executions" + }, + "errors": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Errors", + "description": "Errors raised when executing nodes" + }, + "prepared_source_mapping": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Prepared Source Mapping", + "description": "The map of prepared nodes to original graph nodes" + }, + "source_prepared_mapping": { + "additionalProperties": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + "type": "object", + "title": "Source Prepared Mapping", + "description": "The map of original graph nodes to prepared nodes" + }, + "ready_order": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Ready Order" + }, + "indegree": { + "additionalProperties": { + "type": "integer" + }, + "type": "object", + "title": "Indegree", + "description": "Remaining unmet input count for exec nodes" + } + }, + "type": "object", + "required": [ + "id", + "graph", + "execution_graph", + "executed", + "executed_history", + "results", + "errors", + "prepared_source_mapping", + "source_prepared_mapping" + ], + "title": "GraphExecutionState", + "description": "Tracks source-graph expansion, execution progress, and runtime results." + }, + "GroundingDinoInvocation": { + "category": "segmentation", + "class": "invocation", + "classification": "stable", + "description": "Runs a Grounding DINO model. Performs zero-shot bounding-box object detection from a text prompt.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "anyOf": [ + { + "enum": ["grounding-dino-tiny", "grounding-dino-base"], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The Grounding DINO model to use.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Model" + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The prompt describing the object to segment.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Prompt" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to segment.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "detection_threshold": { + "default": 0.3, + "description": "The detection threshold for the Grounding DINO model. All detected bounding boxes with scores above this threshold will be returned.", + "field_kind": "input", + "input": "any", + "maximum": 1.0, + "minimum": 0.0, + "orig_default": 0.3, + "orig_required": false, + "title": "Detection Threshold", + "type": "number" + }, + "type": { + "const": "grounding_dino", + "default": "grounding_dino", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["prompt", "object detection"], + "title": "Grounding DINO (Text Prompt Object Detection)", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/BoundingBoxCollectionOutput" + } + }, + "HEDEdgeDetectionInvocation": { + "category": "controlnet_preprocessors", + "class": "invocation", + "classification": "stable", + "description": "Geneartes an edge map using the HED (softedge) model.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "scribble": { + "default": false, + "description": "Whether or not to use scribble mode", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Scribble", + "type": "boolean" + }, + "type": { + "const": "hed_edge_detection", + "default": "hed_edge_detection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["controlnet", "hed", "softedge"], + "title": "HED Edge Detection", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "HFModelSource": { + "properties": { + "repo_id": { + "type": "string", + "title": "Repo Id" + }, + "variant": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelRepoVariant" + }, + { + "type": "null" + } + ], + "default": "fp16" + }, + "subfolder": { + "anyOf": [ + { + "type": "string", + "format": "path" + }, + { + "type": "null" + } + ], + "title": "Subfolder" + }, + "access_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Access Token" + }, + "type": { + "type": "string", + "const": "hf", + "title": "Type", + "default": "hf" + } + }, + "type": "object", + "required": ["repo_id"], + "title": "HFModelSource", + "description": "A HuggingFace repo_id with optional variant, sub-folder(s) and access token.\nNote that the variant option, if not provided to the constructor, will default to fp16, which is\nwhat people (almost) always want.\n\nThe subfolder can be a single path or multiple paths joined by '+' (e.g., \"text_encoder+tokenizer\").\nWhen multiple subfolders are specified, all of them will be downloaded and combined into the model directory." + }, + "HFTokenStatus": { + "type": "string", + "enum": ["valid", "invalid", "unknown"], + "title": "HFTokenStatus" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "HeuristicResizeInvocation": { + "category": "controlnet_preprocessors", + "class": "invocation", + "classification": "prototype", + "description": "Resize an image using a heuristic method. Preserves edge maps.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to resize", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "width": { + "default": 512, + "description": "The width to resize to (px)", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 512, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 512, + "description": "The height to resize to (px)", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 512, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "type": { + "const": "heuristic_resize", + "default": "heuristic_resize", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image, controlnet"], + "title": "Heuristic Resize", + "type": "object", + "version": "1.1.1", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "HuggingFaceMetadata": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "model's name" + }, + "files": { + "items": { + "$ref": "#/components/schemas/RemoteModelFile" + }, + "type": "array", + "title": "Files", + "description": "model files and their sizes" + }, + "type": { + "type": "string", + "const": "huggingface", + "title": "Type", + "default": "huggingface" + }, + "id": { + "type": "string", + "title": "Id", + "description": "The HF model id" + }, + "api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Api Response", + "description": "Response from the HF API as stringified JSON" + }, + "is_diffusers": { + "type": "boolean", + "title": "Is Diffusers", + "description": "Whether the metadata is for a Diffusers format model", + "default": false + }, + "ckpt_urls": { + "anyOf": [ + { + "items": { + "type": "string", + "minLength": 1, + "format": "uri" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Ckpt Urls", + "description": "URLs for all checkpoint format models in the metadata" + } + }, + "type": "object", + "required": ["name", "id"], + "title": "HuggingFaceMetadata", + "description": "Extended metadata fields provided by HuggingFace." + }, + "HuggingFaceModels": { + "properties": { + "urls": { + "anyOf": [ + { + "items": { + "type": "string", + "minLength": 1, + "format": "uri" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Urls", + "description": "URLs for all checkpoint format models in the metadata" + }, + "is_diffusers": { + "type": "boolean", + "title": "Is Diffusers", + "description": "Whether the metadata is for a Diffusers format model" + } + }, + "type": "object", + "required": ["urls", "is_diffusers"], + "title": "HuggingFaceModels" + }, + "IPAdapterField": { + "properties": { + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "items": { + "$ref": "#/components/schemas/ImageField" + }, + "type": "array" + } + ], + "description": "The IP-Adapter image prompt(s).", + "title": "Image" + }, + "ip_adapter_model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "The IP-Adapter model to use." + }, + "image_encoder_model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "The name of the CLIP image encoder model." + }, + "weight": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 1, + "description": "The weight given to the IP-Adapter.", + "title": "Weight" + }, + "target_blocks": { + "default": [], + "description": "The IP Adapter blocks to apply", + "items": { + "type": "string" + }, + "title": "Target Blocks", + "type": "array" + }, + "method": { + "default": "full", + "description": "Weight apply method", + "title": "Method", + "type": "string" + }, + "begin_step_percent": { + "default": 0, + "description": "When the IP-Adapter is first applied (% of total steps)", + "maximum": 1, + "minimum": 0, + "title": "Begin Step Percent", + "type": "number" + }, + "end_step_percent": { + "default": 1, + "description": "When the IP-Adapter is last applied (% of total steps)", + "maximum": 1, + "minimum": 0, + "title": "End Step Percent", + "type": "number" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The bool mask associated with this IP-Adapter. Excluded regions should be set to False, included regions should be set to True." + } + }, + "required": ["image", "ip_adapter_model", "image_encoder_model"], + "title": "IPAdapterField", + "type": "object" + }, + "IPAdapterInvocation": { + "category": "conditioning", + "class": "invocation", + "classification": "stable", + "description": "Collects IP-Adapter info to pass to other nodes.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "items": { + "$ref": "#/components/schemas/ImageField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The IP-Adapter image prompt(s).", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Image", + "ui_order": 1 + }, + "ip_adapter_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The IP-Adapter model.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "IP-Adapter Model", + "ui_model_base": ["sd-1", "sdxl"], + "ui_model_type": ["ip_adapter"], + "ui_order": -1 + }, + "clip_vision_model": { + "default": "ViT-H", + "description": "CLIP Vision model to use. Overrides model settings. Mandatory for checkpoint models.", + "enum": ["ViT-H", "ViT-G", "ViT-L"], + "field_kind": "input", + "input": "any", + "orig_default": "ViT-H", + "orig_required": false, + "title": "Clip Vision Model", + "type": "string", + "ui_order": 2 + }, + "weight": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 1, + "description": "The weight given to the IP-Adapter", + "field_kind": "input", + "input": "any", + "orig_default": 1, + "orig_required": false, + "title": "Weight" + }, + "method": { + "default": "full", + "description": "The method to apply the IP-Adapter", + "enum": ["full", "style", "composition", "style_strong", "style_precise"], + "field_kind": "input", + "input": "any", + "orig_default": "full", + "orig_required": false, + "title": "Method", + "type": "string" + }, + "begin_step_percent": { + "default": 0, + "description": "When the IP-Adapter is first applied (% of total steps)", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Begin Step Percent", + "type": "number" + }, + "end_step_percent": { + "default": 1, + "description": "When the IP-Adapter is last applied (% of total steps)", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1, + "orig_required": false, + "title": "End Step Percent", + "type": "number" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A mask defining the region that this IP-Adapter applies to.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "type": { + "const": "ip_adapter", + "default": "ip_adapter", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["ip_adapter", "control"], + "title": "IP-Adapter - SD1.5, SDXL", + "type": "object", + "version": "1.5.1", + "output": { + "$ref": "#/components/schemas/IPAdapterOutput" + } + }, + "IPAdapterMetadataField": { + "description": "IP Adapter Field, minus the CLIP Vision Encoder model", + "properties": { + "image": { + "$ref": "#/components/schemas/ImageField", + "description": "The IP-Adapter image prompt." + }, + "ip_adapter_model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "The IP-Adapter model." + }, + "clip_vision_model": { + "description": "The CLIP Vision model", + "enum": ["ViT-L", "ViT-H", "ViT-G"], + "title": "Clip Vision Model", + "type": "string" + }, + "method": { + "description": "Method to apply IP Weights with", + "enum": ["full", "style", "composition", "style_strong", "style_precise"], + "title": "Method", + "type": "string" + }, + "weight": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "description": "The weight given to the IP-Adapter", + "title": "Weight" + }, + "begin_step_percent": { + "description": "When the IP-Adapter is first applied (% of total steps)", + "title": "Begin Step Percent", + "type": "number" + }, + "end_step_percent": { + "description": "When the IP-Adapter is last applied (% of total steps)", + "title": "End Step Percent", + "type": "number" + } + }, + "required": [ + "image", + "ip_adapter_model", + "clip_vision_model", + "method", + "weight", + "begin_step_percent", + "end_step_percent" + ], + "title": "IPAdapterMetadataField", + "type": "object" + }, + "IPAdapterOutput": { + "class": "output", + "properties": { + "ip_adapter": { + "$ref": "#/components/schemas/IPAdapterField", + "description": "IP-Adapter to apply", + "field_kind": "output", + "title": "IP-Adapter", + "ui_hidden": false + }, + "type": { + "const": "ip_adapter_output", + "default": "ip_adapter_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "ip_adapter", "type", "type"], + "title": "IPAdapterOutput", + "type": "object" + }, + "IPAdapterRecallParameter": { + "properties": { + "model_name": { + "type": "string", + "title": "Model Name", + "description": "The name of the IP Adapter model" + }, + "image_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Image Name", + "description": "The filename of the reference image in outputs/images" + }, + "weight": { + "type": "number", + "maximum": 2.0, + "minimum": -1.0, + "title": "Weight", + "description": "The weight for the IP Adapter", + "default": 1.0 + }, + "begin_step_percent": { + "anyOf": [ + { + "type": "number", + "maximum": 1.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Begin Step Percent", + "description": "When the IP Adapter is first applied (% of total steps)" + }, + "end_step_percent": { + "anyOf": [ + { + "type": "number", + "maximum": 1.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "End Step Percent", + "description": "When the IP Adapter is last applied (% of total steps)" + }, + "method": { + "anyOf": [ + { + "type": "string", + "enum": ["full", "style", "composition"] + }, + { + "type": "null" + } + ], + "title": "Method", + "description": "The IP Adapter method" + }, + "image_influence": { + "anyOf": [ + { + "type": "string", + "enum": ["lowest", "low", "medium", "high", "highest"] + }, + { + "type": "null" + } + ], + "title": "Image Influence", + "description": "FLUX Redux image influence (if model is flux_redux)" + } + }, + "type": "object", + "required": ["model_name"], + "title": "IPAdapterRecallParameter", + "description": "IP Adapter configuration for recall" + }, + "IPAdapter_Checkpoint_FLUX_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "ip_adapter", + "title": "Type", + "default": "ip_adapter" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "base": { + "type": "string", + "const": "flux", + "title": "Base", + "default": "flux" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "format", + "base" + ], + "title": "IPAdapter_Checkpoint_FLUX_Config" + }, + "IPAdapter_Checkpoint_SD1_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "ip_adapter", + "title": "Type", + "default": "ip_adapter" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "base": { + "type": "string", + "const": "sd-1", + "title": "Base", + "default": "sd-1" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "format", + "base" + ], + "title": "IPAdapter_Checkpoint_SD1_Config" + }, + "IPAdapter_Checkpoint_SD2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "ip_adapter", + "title": "Type", + "default": "ip_adapter" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "base": { + "type": "string", + "const": "sd-2", + "title": "Base", + "default": "sd-2" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "format", + "base" + ], + "title": "IPAdapter_Checkpoint_SD2_Config" + }, + "IPAdapter_Checkpoint_SDXL_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "ip_adapter", + "title": "Type", + "default": "ip_adapter" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "base": { + "type": "string", + "const": "sdxl", + "title": "Base", + "default": "sdxl" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "format", + "base" + ], + "title": "IPAdapter_Checkpoint_SDXL_Config" + }, + "IPAdapter_InvokeAI_SD1_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "ip_adapter", + "title": "Type", + "default": "ip_adapter" + }, + "format": { + "type": "string", + "const": "invokeai", + "title": "Format", + "default": "invokeai" + }, + "image_encoder_model_id": { + "type": "string", + "title": "Image Encoder Model Id" + }, + "base": { + "type": "string", + "const": "sd-1", + "title": "Base", + "default": "sd-1" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "format", + "image_encoder_model_id", + "base" + ], + "title": "IPAdapter_InvokeAI_SD1_Config" + }, + "IPAdapter_InvokeAI_SD2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "ip_adapter", + "title": "Type", + "default": "ip_adapter" + }, + "format": { + "type": "string", + "const": "invokeai", + "title": "Format", + "default": "invokeai" + }, + "image_encoder_model_id": { + "type": "string", + "title": "Image Encoder Model Id" + }, + "base": { + "type": "string", + "const": "sd-2", + "title": "Base", + "default": "sd-2" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "format", + "image_encoder_model_id", + "base" + ], + "title": "IPAdapter_InvokeAI_SD2_Config" + }, + "IPAdapter_InvokeAI_SDXL_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "ip_adapter", + "title": "Type", + "default": "ip_adapter" + }, + "format": { + "type": "string", + "const": "invokeai", + "title": "Format", + "default": "invokeai" + }, + "image_encoder_model_id": { + "type": "string", + "title": "Image Encoder Model Id" + }, + "base": { + "type": "string", + "const": "sdxl", + "title": "Base", + "default": "sdxl" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "format", + "image_encoder_model_id", + "base" + ], + "title": "IPAdapter_InvokeAI_SDXL_Config" + }, + "IdealSizeInvocation": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Calculates the ideal size for generation to avoid duplication", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "width": { + "default": 1024, + "description": "Final image width", + "field_kind": "input", + "input": "any", + "orig_default": 1024, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 576, + "description": "Final image height", + "field_kind": "input", + "input": "any", + "orig_default": 576, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "unet": { + "anyOf": [ + { + "$ref": "#/components/schemas/UNetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "UNet (scheduler, LoRAs)", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "multiplier": { + "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)", + "field_kind": "input", + "input": "any", + "orig_default": 1.0, + "orig_required": false, + "title": "Multiplier", + "type": "number" + }, + "type": { + "const": "ideal_size", + "default": "ideal_size", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "math", "ideal_size"], + "title": "Ideal Size - SD1.5, SDXL", + "type": "object", + "version": "1.0.6", + "output": { + "$ref": "#/components/schemas/IdealSizeOutput" + } + }, + "IdealSizeOutput": { + "class": "output", + "description": "Base class for invocations that output an image", + "properties": { + "width": { + "description": "The ideal width of the image (in pixels)", + "field_kind": "output", + "title": "Width", + "type": "integer", + "ui_hidden": false + }, + "height": { + "description": "The ideal height of the image (in pixels)", + "field_kind": "output", + "title": "Height", + "type": "integer", + "ui_hidden": false + }, + "type": { + "const": "ideal_size_output", + "default": "ideal_size_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "width", "height", "type", "type"], + "title": "IdealSizeOutput", + "type": "object" + }, + "IfInvocation": { + "category": "math", + "class": "invocation", + "classification": "stable", + "description": "Selects between two optional inputs based on a boolean condition.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "condition": { + "default": false, + "description": "The condition used to select an input", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Condition", + "type": "boolean" + }, + "true_input": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "description": "Selected when the condition is true", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "True Input", + "ui_type": "AnyField" + }, + "false_input": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "description": "Selected when the condition is false", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "False Input", + "ui_type": "AnyField" + }, + "type": { + "const": "if", + "default": "if", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["logic", "conditional"], + "title": "If", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/IfInvocationOutput" + } + }, + "IfInvocationOutput": { + "class": "output", + "properties": { + "value": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "description": "The selected value", + "field_kind": "output", + "title": "Output", + "ui_hidden": false, + "ui_type": "AnyField" + }, + "type": { + "const": "if_output", + "default": "if_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "value", "type", "type"], + "title": "IfInvocationOutput", + "type": "object" + }, + "ImageBatchInvocation": { + "category": "batch", + "class": "invocation", + "classification": "special", + "description": "Create a batched generation, where the workflow is executed once for each image in the batch.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "batch_group_id": { + "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.", + "enum": ["None", "Group 1", "Group 2", "Group 3", "Group 4", "Group 5"], + "field_kind": "input", + "input": "direct", + "orig_default": "None", + "orig_required": false, + "title": "Batch Group", + "type": "string" + }, + "images": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ImageField" + }, + "minItems": 1, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The images to batch over", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Images" + }, + "type": { + "const": "image_batch", + "default": "image_batch", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "image", "batch", "special"], + "title": "Image Batch", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageBlurInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Blurs an image", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to blur", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "radius": { + "default": 8.0, + "description": "The blur radius", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 8.0, + "orig_required": false, + "title": "Radius", + "type": "number" + }, + "blur_type": { + "default": "gaussian", + "description": "The type of blur", + "enum": ["gaussian", "box"], + "field_kind": "input", + "input": "any", + "orig_default": "gaussian", + "orig_required": false, + "title": "Blur Type", + "type": "string" + }, + "type": { + "const": "img_blur", + "default": "img_blur", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "blur"], + "title": "Blur Image", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageCategory": { + "type": "string", + "enum": ["general", "mask", "control", "user", "other"], + "title": "ImageCategory", + "description": "The category of an image.\n\n- GENERAL: The image is an output, init image, or otherwise an image without a specialized purpose.\n- MASK: The image is a mask image.\n- CONTROL: The image is a ControlNet control image.\n- USER: The image is a user-provide image.\n- OTHER: The image is some other type of image with a specialized purpose. To be used by external nodes." + }, + "ImageChannelInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Gets a channel from an image.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to get the channel from", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "channel": { + "default": "A", + "description": "The channel to get", + "enum": ["A", "R", "G", "B"], + "field_kind": "input", + "input": "any", + "orig_default": "A", + "orig_required": false, + "title": "Channel", + "type": "string" + }, + "type": { + "const": "img_chan", + "default": "img_chan", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "channel"], + "title": "Extract Image Channel", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageChannelMultiplyInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Scale a specific color channel of an image.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to adjust", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "channel": { + "anyOf": [ + { + "enum": [ + "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)" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Which channel to adjust", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Channel" + }, + "scale": { + "default": 1.0, + "description": "The amount to scale the channel by.", + "field_kind": "input", + "input": "any", + "minimum": 0.0, + "orig_default": 1.0, + "orig_required": false, + "title": "Scale", + "type": "number" + }, + "invert_channel": { + "default": false, + "description": "Invert the channel after scaling", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Invert Channel", + "type": "boolean" + }, + "type": { + "const": "img_channel_multiply", + "default": "img_channel_multiply", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": [ + "image", + "invert", + "scale", + "multiply", + "red", + "green", + "blue", + "alpha", + "cyan", + "magenta", + "yellow", + "black", + "hue", + "saturation", + "luminosity", + "value" + ], + "title": "Multiply Image Channel", + "type": "object", + "version": "1.2.3", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageChannelOffsetInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Add or subtract a value from a specific color channel of an image.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to adjust", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "channel": { + "anyOf": [ + { + "enum": [ + "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)" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Which channel to adjust", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Channel" + }, + "offset": { + "default": 0, + "description": "The amount to adjust the channel by", + "field_kind": "input", + "input": "any", + "maximum": 255, + "minimum": -255, + "orig_default": 0, + "orig_required": false, + "title": "Offset", + "type": "integer" + }, + "type": { + "const": "img_channel_offset", + "default": "img_channel_offset", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": [ + "image", + "offset", + "red", + "green", + "blue", + "alpha", + "cyan", + "magenta", + "yellow", + "black", + "hue", + "saturation", + "luminosity", + "value" + ], + "title": "Offset Image Channel", + "type": "object", + "version": "1.2.3", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageCollectionInvocation": { + "category": "primitives", + "class": "invocation", + "classification": "stable", + "description": "A collection of image primitive values", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "collection": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ImageField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The collection of image values", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Collection" + }, + "type": { + "const": "image_collection", + "default": "image_collection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "image", "collection"], + "title": "Image Collection Primitive", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/ImageCollectionOutput" + } + }, + "ImageCollectionOutput": { + "class": "output", + "description": "Base class for nodes that output a collection of images", + "properties": { + "collection": { + "description": "The output images", + "field_kind": "output", + "items": { + "$ref": "#/components/schemas/ImageField" + }, + "title": "Collection", + "type": "array", + "ui_hidden": false + }, + "type": { + "const": "image_collection_output", + "default": "image_collection_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "collection", "type", "type"], + "title": "ImageCollectionOutput", + "type": "object" + }, + "ImageConvertInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Converts an image to a different mode.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to convert", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "mode": { + "default": "L", + "description": "The mode to convert to", + "enum": ["L", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F"], + "field_kind": "input", + "input": "any", + "orig_default": "L", + "orig_required": false, + "title": "Mode", + "type": "string" + }, + "type": { + "const": "img_conv", + "default": "img_conv", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "convert"], + "title": "Convert Image Mode", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageCropInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Crops an image to a specified box. The box can be outside of the image.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to crop", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "x": { + "default": 0, + "description": "The left x coordinate of the crop rectangle", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "X", + "type": "integer" + }, + "y": { + "default": 0, + "description": "The top y coordinate of the crop rectangle", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Y", + "type": "integer" + }, + "width": { + "default": 512, + "description": "The width of the crop rectangle", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 512, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 512, + "description": "The height of the crop rectangle", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 512, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "type": { + "const": "img_crop", + "default": "img_crop", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "crop"], + "title": "Crop Image", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageDTO": { + "properties": { + "image_name": { + "type": "string", + "title": "Image Name", + "description": "The unique name of the image." + }, + "image_url": { + "type": "string", + "title": "Image Url", + "description": "The URL of the image." + }, + "thumbnail_url": { + "type": "string", + "title": "Thumbnail Url", + "description": "The URL of the image's thumbnail." + }, + "image_origin": { + "$ref": "#/components/schemas/ResourceOrigin", + "description": "The type of the image." + }, + "image_category": { + "$ref": "#/components/schemas/ImageCategory", + "description": "The category of the image." + }, + "width": { + "type": "integer", + "title": "Width", + "description": "The width of the image in px." + }, + "height": { + "type": "integer", + "title": "Height", + "description": "The height of the image in px." + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + } + ], + "title": "Created At", + "description": "The created timestamp of the image." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + } + ], + "title": "Updated At", + "description": "The updated timestamp of the image." + }, + "deleted_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Deleted At", + "description": "The deleted timestamp of the image." + }, + "is_intermediate": { + "type": "boolean", + "title": "Is Intermediate", + "description": "Whether this is an intermediate image." + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Session Id", + "description": "The session ID that generated this image, if it is a generated image." + }, + "node_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node Id", + "description": "The node ID that generated this image, if it is a generated image." + }, + "starred": { + "type": "boolean", + "title": "Starred", + "description": "Whether this image is starred." + }, + "has_workflow": { + "type": "boolean", + "title": "Has Workflow", + "description": "Whether this image has a workflow." + }, + "image_subfolder": { + "type": "string", + "title": "Image Subfolder", + "description": "The subfolder where the image is stored on disk.", + "default": "" + }, + "board_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Board Id", + "description": "The id of the board the image belongs to, if one exists." + } + }, + "type": "object", + "required": [ + "image_name", + "image_url", + "thumbnail_url", + "image_origin", + "image_category", + "width", + "height", + "created_at", + "updated_at", + "is_intermediate", + "starred", + "has_workflow" + ], + "title": "ImageDTO", + "description": "Deserialized image record, enriched for the frontend." + }, + "ImageField": { + "properties": { + "image_name": { + "type": "string", + "title": "Image Name", + "description": "The name of the image" + } + }, + "type": "object", + "required": ["image_name"], + "title": "ImageField", + "description": "An image primitive field" + }, + "ImageGenerator": { + "category": "batch", + "class": "invocation", + "classification": "special", + "description": "Generated a collection of images for use in a batched generation", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "generator": { + "$ref": "#/components/schemas/ImageGeneratorField", + "description": "The image generator.", + "field_kind": "input", + "input": "direct", + "orig_required": true, + "title": "Generator Type" + }, + "type": { + "const": "image_generator", + "default": "image_generator", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["generator", "type", "id"], + "tags": ["primitives", "board", "image", "batch", "special"], + "title": "Image Generator", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageGeneratorOutput" + } + }, + "ImageGeneratorField": { + "properties": {}, + "title": "ImageGeneratorField", + "type": "object" + }, + "ImageGeneratorOutput": { + "class": "output", + "description": "Base class for nodes that output a collection of boards", + "properties": { + "images": { + "description": "The generated images", + "field_kind": "output", + "items": { + "$ref": "#/components/schemas/ImageField" + }, + "title": "Images", + "type": "array", + "ui_hidden": false + }, + "type": { + "const": "image_generator_output", + "default": "image_generator_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "images", "type", "type"], + "title": "ImageGeneratorOutput", + "type": "object" + }, + "ImageHueAdjustmentInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Adjusts the Hue of an image.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to adjust", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "hue": { + "default": 0, + "description": "The degrees by which to rotate the hue, 0-360", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Hue", + "type": "integer" + }, + "type": { + "const": "img_hue_adjust", + "default": "img_hue_adjust", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "hue"], + "title": "Adjust Image Hue", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageInverseLerpInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Inverse linear interpolation of all pixels of an image", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to lerp", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "min": { + "default": 0, + "description": "The minimum input value", + "field_kind": "input", + "input": "any", + "maximum": 255, + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Min", + "type": "integer" + }, + "max": { + "default": 255, + "description": "The maximum input value", + "field_kind": "input", + "input": "any", + "maximum": 255, + "minimum": 0, + "orig_default": 255, + "orig_required": false, + "title": "Max", + "type": "integer" + }, + "type": { + "const": "img_ilerp", + "default": "img_ilerp", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "ilerp"], + "title": "Inverse Lerp Image", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageInvocation": { + "category": "primitives", + "class": "invocation", + "classification": "stable", + "description": "An image primitive value", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to load", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "image", + "default": "image", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "image"], + "title": "Image Primitive", + "type": "object", + "version": "1.0.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageLerpInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Linear interpolation of all pixels of an image", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to lerp", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "min": { + "default": 0, + "description": "The minimum output value", + "field_kind": "input", + "input": "any", + "maximum": 255, + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Min", + "type": "integer" + }, + "max": { + "default": 255, + "description": "The maximum output value", + "field_kind": "input", + "input": "any", + "maximum": 255, + "minimum": 0, + "orig_default": 255, + "orig_required": false, + "title": "Max", + "type": "integer" + }, + "type": { + "const": "img_lerp", + "default": "img_lerp", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "lerp"], + "title": "Lerp Image", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageMaskToTensorInvocation": { + "category": "mask", + "class": "invocation", + "classification": "stable", + "description": "Convert a mask image to a tensor. Converts the image to grayscale and uses thresholding at the specified value.", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The mask image to convert.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "cutoff": { + "default": 128, + "description": "Cutoff (<)", + "field_kind": "input", + "input": "any", + "maximum": 255, + "minimum": 0, + "orig_default": 128, + "orig_required": false, + "title": "Cutoff", + "type": "integer" + }, + "invert": { + "default": false, + "description": "Whether to invert the mask.", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Invert", + "type": "boolean" + }, + "type": { + "const": "image_mask_to_tensor", + "default": "image_mask_to_tensor", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["conditioning"], + "title": "Image Mask to Tensor", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/MaskOutput" + } + }, + "ImageMultiplyInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Multiplies two images together using `PIL.ImageChops.multiply()`.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image1": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The first image to multiply", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "image2": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The second image to multiply", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "img_mul", + "default": "img_mul", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "multiply"], + "title": "Multiply Images", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageNSFWBlurInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Add blur to NSFW-flagged images", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to check", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "img_nsfw", + "default": "img_nsfw", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "nsfw"], + "title": "Blur NSFW Image", + "type": "object", + "version": "1.2.3", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageNamesResult": { + "properties": { + "image_names": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Image Names", + "description": "Ordered list of image names" + }, + "starred_count": { + "type": "integer", + "title": "Starred Count", + "description": "Number of starred images (when starred_first=True)" + }, + "total_count": { + "type": "integer", + "title": "Total Count", + "description": "Total number of images matching the query" + } + }, + "type": "object", + "required": ["image_names", "starred_count", "total_count"], + "title": "ImageNamesResult", + "description": "Response containing ordered image names with metadata for optimistic updates." + }, + "ImageNoiseInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Add noise to an image", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to add noise to", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional mask determining where to apply noise (black=noise, white=no noise)", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "seed": { + "default": 0, + "description": "Seed for random number generation", + "field_kind": "input", + "input": "any", + "maximum": 4294967295, + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Seed", + "type": "integer" + }, + "noise_type": { + "default": "gaussian", + "description": "The type of noise to add", + "enum": ["gaussian", "salt_and_pepper"], + "field_kind": "input", + "input": "any", + "orig_default": "gaussian", + "orig_required": false, + "title": "Noise Type", + "type": "string" + }, + "amount": { + "default": 0.1, + "description": "The amount of noise to add", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0.1, + "orig_required": false, + "title": "Amount", + "type": "number" + }, + "noise_color": { + "default": true, + "description": "Whether to add colored noise", + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "Noise Color", + "type": "boolean" + }, + "size": { + "default": 1, + "description": "The size of the noise points", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 1, + "orig_required": false, + "title": "Size", + "type": "integer" + }, + "type": { + "const": "img_noise", + "default": "img_noise", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "noise"], + "title": "Add Image Noise", + "type": "object", + "version": "1.1.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageOutput": { + "class": "output", + "description": "Base class for nodes that output a single image", + "properties": { + "image": { + "$ref": "#/components/schemas/ImageField", + "description": "The output image", + "field_kind": "output", + "ui_hidden": false + }, + "width": { + "description": "The width of the image in pixels", + "field_kind": "output", + "title": "Width", + "type": "integer", + "ui_hidden": false + }, + "height": { + "description": "The height of the image in pixels", + "field_kind": "output", + "title": "Height", + "type": "integer", + "ui_hidden": false + }, + "type": { + "const": "image_output", + "default": "image_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "image", "width", "height", "type", "type"], + "title": "ImageOutput", + "type": "object" + }, + "ImagePanelCoordinateOutput": { + "class": "output", + "properties": { + "x_left": { + "description": "The left x-coordinate of the panel.", + "field_kind": "output", + "title": "X Left", + "type": "integer", + "ui_hidden": false + }, + "y_top": { + "description": "The top y-coordinate of the panel.", + "field_kind": "output", + "title": "Y Top", + "type": "integer", + "ui_hidden": false + }, + "width": { + "description": "The width of the panel.", + "field_kind": "output", + "title": "Width", + "type": "integer", + "ui_hidden": false + }, + "height": { + "description": "The height of the panel.", + "field_kind": "output", + "title": "Height", + "type": "integer", + "ui_hidden": false + }, + "type": { + "const": "image_panel_coordinate_output", + "default": "image_panel_coordinate_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "x_left", "y_top", "width", "height", "type", "type"], + "title": "ImagePanelCoordinateOutput", + "type": "object" + }, + "ImagePanelLayoutInvocation": { + "category": "canvas", + "class": "invocation", + "classification": "prototype", + "description": "Get the coordinates of a single panel in a grid. (If the full image shape cannot be divided evenly into panels,\nthen the grid may not cover the entire image.)", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "width": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The width of the entire grid.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Width" + }, + "height": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The height of the entire grid.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Height" + }, + "num_cols": { + "default": 1, + "description": "The number of columns in the grid.", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 1, + "orig_required": false, + "title": "Num Cols", + "type": "integer" + }, + "num_rows": { + "default": 1, + "description": "The number of rows in the grid.", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 1, + "orig_required": false, + "title": "Num Rows", + "type": "integer" + }, + "panel_col_idx": { + "default": 0, + "description": "The column index of the panel to be processed.", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Panel Col Idx", + "type": "integer" + }, + "panel_row_idx": { + "default": 0, + "description": "The row index of the panel to be processed.", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Panel Row Idx", + "type": "integer" + }, + "type": { + "const": "image_panel_layout", + "default": "image_panel_layout", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "panel", "layout"], + "title": "Image Panel Layout", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImagePanelCoordinateOutput" + } + }, + "ImagePasteInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Pastes an image into another image.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "base_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The base image", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to paste", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The mask to use when pasting", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "x": { + "default": 0, + "description": "The left x coordinate at which to paste the image", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "X", + "type": "integer" + }, + "y": { + "default": 0, + "description": "The top y coordinate at which to paste the image", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Y", + "type": "integer" + }, + "crop": { + "default": false, + "description": "Crop to base image dimensions", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Crop", + "type": "boolean" + }, + "type": { + "const": "img_paste", + "default": "img_paste", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "paste"], + "title": "Paste Image", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageRecordChanges": { + "properties": { + "image_category": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageCategory" + }, + { + "type": "null" + } + ], + "description": "The image's new category." + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Session Id", + "description": "The image's new session ID." + }, + "is_intermediate": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Intermediate", + "description": "The image's new `is_intermediate` flag." + }, + "starred": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Starred", + "description": "The image's new `starred` state" + } + }, + "additionalProperties": true, + "type": "object", + "title": "ImageRecordChanges", + "description": "A set of changes to apply to an image record.\n\nOnly limited changes are valid:\n - `image_category`: change the category of an image\n - `session_id`: change the session associated with an image\n - `is_intermediate`: change the image's `is_intermediate` flag\n - `starred`: change whether the image is starred" + }, + "ImageResizeInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Resizes an image to specific dimensions", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to resize", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "width": { + "default": 512, + "description": "The width to resize to (px)", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 512, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 512, + "description": "The height to resize to (px)", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 512, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "resample_mode": { + "default": "bicubic", + "description": "The resampling mode", + "enum": ["nearest", "box", "bilinear", "hamming", "bicubic", "lanczos"], + "field_kind": "input", + "input": "any", + "orig_default": "bicubic", + "orig_required": false, + "title": "Resample Mode", + "type": "string" + }, + "type": { + "const": "img_resize", + "default": "img_resize", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "resize"], + "title": "Resize Image", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageScaleInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Scales an image by a factor", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to scale", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "scale_factor": { + "default": 2.0, + "description": "The factor by which to scale the image", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 2.0, + "orig_required": false, + "title": "Scale Factor", + "type": "number" + }, + "resample_mode": { + "default": "bicubic", + "description": "The resampling mode", + "enum": ["nearest", "box", "bilinear", "hamming", "bicubic", "lanczos"], + "field_kind": "input", + "input": "any", + "orig_default": "bicubic", + "orig_required": false, + "title": "Resample Mode", + "type": "string" + }, + "type": { + "const": "img_scale", + "default": "img_scale", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "scale"], + "title": "Scale Image", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImageToLatentsInvocation": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Encodes an image into latents.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to encode", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "tiled": { + "default": false, + "description": "Processing using overlapping tiles (reduce memory consumption)", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Tiled", + "type": "boolean" + }, + "tile_size": { + "default": 0, + "description": "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.", + "field_kind": "input", + "input": "any", + "multipleOf": 8, + "orig_default": 0, + "orig_required": false, + "title": "Tile Size", + "type": "integer" + }, + "fp32": { + "default": false, + "description": "Whether or not to use full float32 precision", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Fp32", + "type": "boolean" + }, + "color_compensation": { + "default": "None", + "description": "Apply VAE scaling compensation when encoding images (reduces color drift).", + "enum": ["None", "SDXL"], + "field_kind": "input", + "input": "any", + "orig_default": "None", + "orig_required": false, + "title": "Color Compensation", + "type": "string" + }, + "type": { + "const": "i2l", + "default": "i2l", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "image", "vae", "i2l"], + "title": "Image to Latents - SD1.5, SDXL", + "type": "object", + "version": "1.2.0", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "ImageToPromptRequest": { + "properties": { + "image_name": { + "type": "string", + "title": "Image Name" + }, + "model_key": { + "type": "string", + "title": "Model Key" + }, + "instruction": { + "type": "string", + "title": "Instruction", + "default": "Describe this image in detail for use as an AI image generation prompt." + } + }, + "type": "object", + "required": ["image_name", "model_key"], + "title": "ImageToPromptRequest" + }, + "ImageToPromptResponse": { + "properties": { + "prompt": { + "type": "string", + "title": "Prompt" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + } + }, + "type": "object", + "required": ["prompt"], + "title": "ImageToPromptResponse" + }, + "ImageUploadEntry": { + "properties": { + "image_dto": { + "$ref": "#/components/schemas/ImageDTO", + "description": "The image DTO" + }, + "presigned_url": { + "type": "string", + "title": "Presigned Url", + "description": "The URL to get the presigned URL for the image upload" + } + }, + "type": "object", + "required": ["image_dto", "presigned_url"], + "title": "ImageUploadEntry" + }, + "ImageUrlsDTO": { + "properties": { + "image_name": { + "type": "string", + "title": "Image Name", + "description": "The unique name of the image." + }, + "image_url": { + "type": "string", + "title": "Image Url", + "description": "The URL of the image." + }, + "thumbnail_url": { + "type": "string", + "title": "Thumbnail Url", + "description": "The URL of the image's thumbnail." + } + }, + "type": "object", + "required": ["image_name", "image_url", "thumbnail_url"], + "title": "ImageUrlsDTO", + "description": "The URLs for an image and its thumbnail." + }, + "ImageWatermarkInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Add an invisible watermark to an image", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to check", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "text": { + "default": "InvokeAI", + "description": "Watermark text", + "field_kind": "input", + "input": "any", + "orig_default": "InvokeAI", + "orig_required": false, + "title": "Text", + "type": "string" + }, + "type": { + "const": "img_watermark", + "default": "img_watermark", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "watermark"], + "title": "Add Invisible Watermark", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ImagesDownloaded": { + "properties": { + "response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Response", + "description": "The message to display to the user when images begin downloading" + }, + "bulk_download_item_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Bulk Download Item Name", + "description": "The name of the bulk download item for which events will be emitted" + } + }, + "type": "object", + "title": "ImagesDownloaded" + }, + "InfillColorInvocation": { + "category": "inpaint", + "class": "invocation", + "classification": "stable", + "description": "Infills transparent areas of an image with a solid color", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "color": { + "$ref": "#/components/schemas/ColorField", + "default": { + "r": 127, + "g": 127, + "b": 127, + "a": 255 + }, + "description": "The color to use to infill", + "field_kind": "input", + "input": "any", + "orig_default": { + "a": 255, + "b": 127, + "g": 127, + "r": 127 + }, + "orig_required": false + }, + "type": { + "const": "infill_rgba", + "default": "infill_rgba", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "inpaint"], + "title": "Solid Color Infill", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "InfillPatchMatchInvocation": { + "category": "inpaint", + "class": "invocation", + "classification": "stable", + "description": "Infills transparent areas of an image using the PatchMatch algorithm", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "downscale": { + "default": 2.0, + "description": "Run patchmatch on downscaled image to speedup infill", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 2.0, + "orig_required": false, + "title": "Downscale", + "type": "number" + }, + "resample_mode": { + "default": "bicubic", + "description": "The resampling mode", + "enum": ["nearest", "box", "bilinear", "hamming", "bicubic", "lanczos"], + "field_kind": "input", + "input": "any", + "orig_default": "bicubic", + "orig_required": false, + "title": "Resample Mode", + "type": "string" + }, + "type": { + "const": "infill_patchmatch", + "default": "infill_patchmatch", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "inpaint"], + "title": "PatchMatch Infill", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "InfillTileInvocation": { + "category": "inpaint", + "class": "invocation", + "classification": "stable", + "description": "Infills transparent areas of an image with tiles of the image", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "tile_size": { + "default": 32, + "description": "The tile size (px)", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 32, + "orig_required": false, + "title": "Tile Size", + "type": "integer" + }, + "seed": { + "default": 0, + "description": "The seed to use for tile generation (omit for random)", + "field_kind": "input", + "input": "any", + "maximum": 4294967295, + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Seed", + "type": "integer" + }, + "type": { + "const": "infill_tile", + "default": "infill_tile", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "inpaint"], + "title": "Tile Infill", + "type": "object", + "version": "1.2.3", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "Input": { + "description": "The type of input a field accepts.\n- `Input.Direct`: The field must have its value provided directly, when the invocation and field are instantiated.\n- `Input.Connection`: The field must have its value provided by a connection.\n- `Input.Any`: The field may have its value provided either directly or by a connection.", + "enum": ["connection", "direct", "any"], + "title": "Input", + "type": "string" + }, + "InputFieldJSONSchemaExtra": { + "description": "Extra attributes to be added to input fields and their OpenAPI schema. Used during graph execution,\nand by the workflow editor during schema parsing and UI rendering.", + "properties": { + "input": { + "$ref": "#/components/schemas/Input" + }, + "field_kind": { + "$ref": "#/components/schemas/FieldKind" + }, + "orig_required": { + "default": true, + "title": "Orig Required", + "type": "boolean" + }, + "default": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "orig_default": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Orig Default" + }, + "ui_hidden": { + "default": false, + "title": "Ui Hidden", + "type": "boolean" + }, + "ui_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/UIType" + }, + { + "type": "null" + } + ], + "default": null + }, + "ui_component": { + "anyOf": [ + { + "$ref": "#/components/schemas/UIComponent" + }, + { + "type": "null" + } + ], + "default": null + }, + "ui_order": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ui Order" + }, + "ui_choice_labels": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ui Choice Labels" + }, + "ui_model_base": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/BaseModelType" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ui Model Base" + }, + "ui_model_type": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ModelType" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ui Model Type" + }, + "ui_model_variant": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/ClipVariantType" + }, + { + "$ref": "#/components/schemas/ModelVariantType" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ui Model Variant" + }, + "ui_model_format": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ModelFormat" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ui Model Format" + }, + "ui_model_provider_id": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ui Model Provider Id" + } + }, + "required": [ + "input", + "field_kind", + "orig_required", + "default", + "orig_default", + "ui_hidden", + "ui_type", + "ui_component", + "ui_order", + "ui_choice_labels", + "ui_model_base", + "ui_model_type", + "ui_model_variant", + "ui_model_format", + "ui_model_provider_id" + ], + "title": "InputFieldJSONSchemaExtra", + "type": "object" + }, + "InstallNodePackRequest": { + "properties": { + "source": { + "type": "string", + "title": "Source", + "description": "Git URL of the node pack to install." + } + }, + "type": "object", + "required": ["source"], + "title": "InstallNodePackRequest", + "description": "Request to install a node pack from a git URL." + }, + "InstallNodePackResponse": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the installed node pack." + }, + "success": { + "type": "boolean", + "title": "Success", + "description": "Whether the installation was successful." + }, + "message": { + "type": "string", + "title": "Message", + "description": "Status message." + }, + "workflows_imported": { + "type": "integer", + "title": "Workflows Imported", + "description": "Number of workflows imported from the pack.", + "default": 0 + }, + "requires_dependencies": { + "type": "boolean", + "title": "Requires Dependencies", + "description": "Whether the pack ships a dependency manifest (requirements.txt or pyproject.toml) that the user must install manually following the pack's documentation.", + "default": false + }, + "dependency_file": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Dependency File", + "description": "Name of the detected dependency manifest file, if any." + } + }, + "type": "object", + "required": ["name", "success", "message"], + "title": "InstallNodePackResponse", + "description": "Response after installing a node pack." + }, + "InstallStatus": { + "type": "string", + "enum": ["waiting", "downloading", "downloads_done", "running", "paused", "completed", "error", "cancelled"], + "title": "InstallStatus", + "description": "State of an install job running in the background." + }, + "IntegerBatchInvocation": { + "category": "batch", + "class": "invocation", + "classification": "special", + "description": "Create a batched generation, where the workflow is executed once for each integer in the batch.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "batch_group_id": { + "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.", + "enum": ["None", "Group 1", "Group 2", "Group 3", "Group 4", "Group 5"], + "field_kind": "input", + "input": "direct", + "orig_default": "None", + "orig_required": false, + "title": "Batch Group", + "type": "string" + }, + "integers": { + "anyOf": [ + { + "items": { + "type": "integer" + }, + "minItems": 1, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The integers to batch over", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Integers" + }, + "type": { + "const": "integer_batch", + "default": "integer_batch", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "integer", "number", "batch", "special"], + "title": "Integer Batch", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/IntegerOutput" + } + }, + "IntegerCollectionInvocation": { + "category": "primitives", + "class": "invocation", + "classification": "stable", + "description": "A collection of integer primitive values", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "collection": { + "default": [], + "description": "The collection of integer values", + "field_kind": "input", + "input": "any", + "items": { + "type": "integer" + }, + "orig_default": [], + "orig_required": false, + "title": "Collection", + "type": "array" + }, + "type": { + "const": "integer_collection", + "default": "integer_collection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "integer", "collection"], + "title": "Integer Collection Primitive", + "type": "object", + "version": "1.0.2", + "output": { + "$ref": "#/components/schemas/IntegerCollectionOutput" + } + }, + "IntegerCollectionOutput": { + "class": "output", + "description": "Base class for nodes that output a collection of integers", + "properties": { + "collection": { + "description": "The int collection", + "field_kind": "output", + "items": { + "type": "integer" + }, + "title": "Collection", + "type": "array", + "ui_hidden": false + }, + "type": { + "const": "integer_collection_output", + "default": "integer_collection_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "collection", "type", "type"], + "title": "IntegerCollectionOutput", + "type": "object" + }, + "IntegerGenerator": { + "category": "batch", + "class": "invocation", + "classification": "special", + "description": "Generated a range of integers for use in a batched generation", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "generator": { + "$ref": "#/components/schemas/IntegerGeneratorField", + "description": "The integer generator.", + "field_kind": "input", + "input": "direct", + "orig_required": true, + "title": "Generator Type" + }, + "type": { + "const": "integer_generator", + "default": "integer_generator", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["generator", "type", "id"], + "tags": ["primitives", "int", "number", "batch", "special"], + "title": "Integer Generator", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/IntegerGeneratorOutput" + } + }, + "IntegerGeneratorField": { + "properties": {}, + "title": "IntegerGeneratorField", + "type": "object" + }, + "IntegerGeneratorOutput": { + "class": "output", + "properties": { + "integers": { + "description": "The generated integers", + "field_kind": "output", + "items": { + "type": "integer" + }, + "title": "Integers", + "type": "array", + "ui_hidden": false + }, + "type": { + "const": "integer_generator_output", + "default": "integer_generator_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "integers", "type", "type"], + "title": "IntegerGeneratorOutput", + "type": "object" + }, + "IntegerInvocation": { + "category": "primitives", + "class": "invocation", + "classification": "stable", + "description": "An integer primitive value", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "value": { + "default": 0, + "description": "The integer value", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Value", + "type": "integer" + }, + "type": { + "const": "integer", + "default": "integer", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "integer"], + "title": "Integer Primitive", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/IntegerOutput" + } + }, + "IntegerMathInvocation": { + "category": "math", + "class": "invocation", + "classification": "stable", + "description": "Performs integer math.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "operation": { + "default": "ADD", + "description": "The operation to perform", + "enum": ["ADD", "SUB", "MUL", "DIV", "EXP", "MOD", "ABS", "MIN", "MAX"], + "field_kind": "input", + "input": "any", + "orig_default": "ADD", + "orig_required": false, + "title": "Operation", + "type": "string", + "ui_choice_labels": { + "ABS": "Absolute Value of A", + "ADD": "Add A+B", + "DIV": "Divide A/B", + "EXP": "Exponentiate A^B", + "MAX": "Maximum(A,B)", + "MIN": "Minimum(A,B)", + "MOD": "Modulus A%B", + "MUL": "Multiply A*B", + "SUB": "Subtract A-B" + } + }, + "a": { + "default": 1, + "description": "The first number", + "field_kind": "input", + "input": "any", + "orig_default": 1, + "orig_required": false, + "title": "A", + "type": "integer" + }, + "b": { + "default": 1, + "description": "The second number", + "field_kind": "input", + "input": "any", + "orig_default": 1, + "orig_required": false, + "title": "B", + "type": "integer" + }, + "type": { + "const": "integer_math", + "default": "integer_math", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": [ + "math", + "integer", + "add", + "subtract", + "multiply", + "divide", + "modulus", + "power", + "absolute value", + "min", + "max" + ], + "title": "Integer Math", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/IntegerOutput" + } + }, + "IntegerOutput": { + "class": "output", + "description": "Base class for nodes that output a single integer", + "properties": { + "value": { + "description": "The output integer", + "field_kind": "output", + "title": "Value", + "type": "integer", + "ui_hidden": false + }, + "type": { + "const": "integer_output", + "default": "integer_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "value", "type", "type"], + "title": "IntegerOutput", + "type": "object" + }, + "InvertTensorMaskInvocation": { + "category": "mask", + "class": "invocation", + "classification": "stable", + "description": "Inverts a tensor mask.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The tensor mask to convert.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "invert_tensor_mask", + "default": "invert_tensor_mask", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["conditioning"], + "title": "Invert Tensor Mask", + "type": "object", + "version": "1.1.0", + "output": { + "$ref": "#/components/schemas/MaskOutput" + } + }, + "InvocationCacheStatus": { + "properties": { + "size": { + "type": "integer", + "title": "Size", + "description": "The current size of the invocation cache" + }, + "hits": { + "type": "integer", + "title": "Hits", + "description": "The number of cache hits" + }, + "misses": { + "type": "integer", + "title": "Misses", + "description": "The number of cache misses" + }, + "enabled": { + "type": "boolean", + "title": "Enabled", + "description": "Whether the invocation cache is enabled" + }, + "max_size": { + "type": "integer", + "title": "Max Size", + "description": "The maximum size of the invocation cache" + } + }, + "type": "object", + "required": ["size", "hits", "misses", "enabled", "max_size"], + "title": "InvocationCacheStatus" + }, + "InvocationCompleteEvent": { + "description": "Event model for invocation_complete", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "queue_id": { + "description": "The ID of the queue", + "title": "Queue Id", + "type": "string" + }, + "item_id": { + "description": "The ID of the queue item", + "title": "Item Id", + "type": "integer" + }, + "batch_id": { + "description": "The ID of the queue batch", + "title": "Batch Id", + "type": "string" + }, + "origin": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The origin of the queue item", + "title": "Origin" + }, + "destination": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The destination of the queue item", + "title": "Destination" + }, + "user_id": { + "default": "system", + "description": "The ID of the user who created the queue item", + "title": "User Id", + "type": "string" + }, + "session_id": { + "description": "The ID of the session (aka graph execution state)", + "title": "Session Id", + "type": "string" + }, + "invocation": { + "description": "The ID of the invocation", + "oneOf": [ + { + "$ref": "#/components/schemas/AddInvocation" + }, + { + "$ref": "#/components/schemas/AlibabaCloudImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/AlphaMaskToTensorInvocation" + }, + { + "$ref": "#/components/schemas/AnimaDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/AnimaImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/AnimaLatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/AnimaLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/AnimaLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/AnimaModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/AnimaTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/ApplyMaskTensorToImageInvocation" + }, + { + "$ref": "#/components/schemas/ApplyMaskToImageInvocation" + }, + { + "$ref": "#/components/schemas/BlankImageInvocation" + }, + { + "$ref": "#/components/schemas/BlendLatentsInvocation" + }, + { + "$ref": "#/components/schemas/BooleanCollectionInvocation" + }, + { + "$ref": "#/components/schemas/BooleanInvocation" + }, + { + "$ref": "#/components/schemas/BoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/CLIPSkipInvocation" + }, + { + "$ref": "#/components/schemas/CV2InfillInvocation" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesEvenSplitInvocation" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesInvocation" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesMinimumOverlapInvocation" + }, + { + "$ref": "#/components/schemas/CannyEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/CanvasOutputInvocation" + }, + { + "$ref": "#/components/schemas/CanvasPasteBackInvocation" + }, + { + "$ref": "#/components/schemas/CanvasV2MaskAndCropInvocation" + }, + { + "$ref": "#/components/schemas/CenterPadCropInvocation" + }, + { + "$ref": "#/components/schemas/CogView4DenoiseInvocation" + }, + { + "$ref": "#/components/schemas/CogView4ImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/CogView4LatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/CogView4ModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/CogView4TextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/CollectInvocation" + }, + { + "$ref": "#/components/schemas/ColorCorrectInvocation" + }, + { + "$ref": "#/components/schemas/ColorInvocation" + }, + { + "$ref": "#/components/schemas/ColorMapInvocation" + }, + { + "$ref": "#/components/schemas/CompelInvocation" + }, + { + "$ref": "#/components/schemas/ConditioningCollectionInvocation" + }, + { + "$ref": "#/components/schemas/ConditioningInvocation" + }, + { + "$ref": "#/components/schemas/ContentShuffleInvocation" + }, + { + "$ref": "#/components/schemas/ControlNetInvocation" + }, + { + "$ref": "#/components/schemas/CoreMetadataInvocation" + }, + { + "$ref": "#/components/schemas/CreateDenoiseMaskInvocation" + }, + { + "$ref": "#/components/schemas/CreateGradientMaskInvocation" + }, + { + "$ref": "#/components/schemas/CropImageToBoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/CropLatentsCoreInvocation" + }, + { + "$ref": "#/components/schemas/CvInpaintInvocation" + }, + { + "$ref": "#/components/schemas/DWOpenposeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/DecodeInvisibleWatermarkInvocation" + }, + { + "$ref": "#/components/schemas/DenoiseLatentsInvocation" + }, + { + "$ref": "#/components/schemas/DenoiseLatentsMetaInvocation" + }, + { + "$ref": "#/components/schemas/DepthAnythingDepthEstimationInvocation" + }, + { + "$ref": "#/components/schemas/DivideInvocation" + }, + { + "$ref": "#/components/schemas/DynamicPromptInvocation" + }, + { + "$ref": "#/components/schemas/ESRGANInvocation" + }, + { + "$ref": "#/components/schemas/ExpandMaskWithFadeInvocation" + }, + { + "$ref": "#/components/schemas/FLUXLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/FaceIdentifierInvocation" + }, + { + "$ref": "#/components/schemas/FaceMaskInvocation" + }, + { + "$ref": "#/components/schemas/FaceOffInvocation" + }, + { + "$ref": "#/components/schemas/FloatBatchInvocation" + }, + { + "$ref": "#/components/schemas/FloatCollectionInvocation" + }, + { + "$ref": "#/components/schemas/FloatGenerator" + }, + { + "$ref": "#/components/schemas/FloatInvocation" + }, + { + "$ref": "#/components/schemas/FloatLinearRangeInvocation" + }, + { + "$ref": "#/components/schemas/FloatMathInvocation" + }, + { + "$ref": "#/components/schemas/FloatToIntegerInvocation" + }, + { + "$ref": "#/components/schemas/Flux2DenoiseInvocation" + }, + { + "$ref": "#/components/schemas/Flux2KleinLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/Flux2KleinLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/Flux2KleinModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/Flux2KleinTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/Flux2VaeDecodeInvocation" + }, + { + "$ref": "#/components/schemas/Flux2VaeEncodeInvocation" + }, + { + "$ref": "#/components/schemas/FluxControlLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/FluxControlNetInvocation" + }, + { + "$ref": "#/components/schemas/FluxDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/FluxDenoiseLatentsMetaInvocation" + }, + { + "$ref": "#/components/schemas/FluxFillInvocation" + }, + { + "$ref": "#/components/schemas/FluxIPAdapterInvocation" + }, + { + "$ref": "#/components/schemas/FluxKontextConcatenateImagesInvocation" + }, + { + "$ref": "#/components/schemas/FluxKontextInvocation" + }, + { + "$ref": "#/components/schemas/FluxLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/FluxModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/FluxReduxInvocation" + }, + { + "$ref": "#/components/schemas/FluxTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/FluxVaeDecodeInvocation" + }, + { + "$ref": "#/components/schemas/FluxVaeEncodeInvocation" + }, + { + "$ref": "#/components/schemas/FreeUInvocation" + }, + { + "$ref": "#/components/schemas/GeminiImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/GetMaskBoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/GroundingDinoInvocation" + }, + { + "$ref": "#/components/schemas/HEDEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/HeuristicResizeInvocation" + }, + { + "$ref": "#/components/schemas/IPAdapterInvocation" + }, + { + "$ref": "#/components/schemas/IdealSizeInvocation" + }, + { + "$ref": "#/components/schemas/IfInvocation" + }, + { + "$ref": "#/components/schemas/ImageBatchInvocation" + }, + { + "$ref": "#/components/schemas/ImageBlurInvocation" + }, + { + "$ref": "#/components/schemas/ImageChannelInvocation" + }, + { + "$ref": "#/components/schemas/ImageChannelMultiplyInvocation" + }, + { + "$ref": "#/components/schemas/ImageChannelOffsetInvocation" + }, + { + "$ref": "#/components/schemas/ImageCollectionInvocation" + }, + { + "$ref": "#/components/schemas/ImageConvertInvocation" + }, + { + "$ref": "#/components/schemas/ImageCropInvocation" + }, + { + "$ref": "#/components/schemas/ImageGenerator" + }, + { + "$ref": "#/components/schemas/ImageHueAdjustmentInvocation" + }, + { + "$ref": "#/components/schemas/ImageInverseLerpInvocation" + }, + { + "$ref": "#/components/schemas/ImageInvocation" + }, + { + "$ref": "#/components/schemas/ImageLerpInvocation" + }, + { + "$ref": "#/components/schemas/ImageMaskToTensorInvocation" + }, + { + "$ref": "#/components/schemas/ImageMultiplyInvocation" + }, + { + "$ref": "#/components/schemas/ImageNSFWBlurInvocation" + }, + { + "$ref": "#/components/schemas/ImageNoiseInvocation" + }, + { + "$ref": "#/components/schemas/ImagePanelLayoutInvocation" + }, + { + "$ref": "#/components/schemas/ImagePasteInvocation" + }, + { + "$ref": "#/components/schemas/ImageResizeInvocation" + }, + { + "$ref": "#/components/schemas/ImageScaleInvocation" + }, + { + "$ref": "#/components/schemas/ImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/ImageWatermarkInvocation" + }, + { + "$ref": "#/components/schemas/InfillColorInvocation" + }, + { + "$ref": "#/components/schemas/InfillPatchMatchInvocation" + }, + { + "$ref": "#/components/schemas/InfillTileInvocation" + }, + { + "$ref": "#/components/schemas/IntegerBatchInvocation" + }, + { + "$ref": "#/components/schemas/IntegerCollectionInvocation" + }, + { + "$ref": "#/components/schemas/IntegerGenerator" + }, + { + "$ref": "#/components/schemas/IntegerInvocation" + }, + { + "$ref": "#/components/schemas/IntegerMathInvocation" + }, + { + "$ref": "#/components/schemas/InvertTensorMaskInvocation" + }, + { + "$ref": "#/components/schemas/InvokeAdjustImageHuePlusInvocation" + }, + { + "$ref": "#/components/schemas/InvokeEquivalentAchromaticLightnessInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageBlendInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageCompositorInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageDilateOrErodeInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageEnhanceInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageValueThresholdsInvocation" + }, + { + "$ref": "#/components/schemas/IterateInvocation" + }, + { + "$ref": "#/components/schemas/LaMaInfillInvocation" + }, + { + "$ref": "#/components/schemas/LatentsCollectionInvocation" + }, + { + "$ref": "#/components/schemas/LatentsInvocation" + }, + { + "$ref": "#/components/schemas/LatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/LineartAnimeEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/LineartEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/LlavaOnevisionVllmInvocation" + }, + { + "$ref": "#/components/schemas/LoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/LoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/LoRASelectorInvocation" + }, + { + "$ref": "#/components/schemas/MLSDDetectionInvocation" + }, + { + "$ref": "#/components/schemas/MainModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/MaskCombineInvocation" + }, + { + "$ref": "#/components/schemas/MaskEdgeInvocation" + }, + { + "$ref": "#/components/schemas/MaskFromAlphaInvocation" + }, + { + "$ref": "#/components/schemas/MaskFromIDInvocation" + }, + { + "$ref": "#/components/schemas/MaskTensorToImageInvocation" + }, + { + "$ref": "#/components/schemas/MediaPipeFaceDetectionInvocation" + }, + { + "$ref": "#/components/schemas/MergeMetadataInvocation" + }, + { + "$ref": "#/components/schemas/MergeTilesToImageInvocation" + }, + { + "$ref": "#/components/schemas/MetadataFieldExtractorInvocation" + }, + { + "$ref": "#/components/schemas/MetadataFromImageInvocation" + }, + { + "$ref": "#/components/schemas/MetadataInvocation" + }, + { + "$ref": "#/components/schemas/MetadataItemInvocation" + }, + { + "$ref": "#/components/schemas/MetadataItemLinkedInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToBoolCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToBoolInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToControlnetsInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToFloatCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToFloatInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToIPAdaptersInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToIntegerCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToIntegerInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToLorasCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToLorasInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToModelInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToSDXLLorasInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToSDXLModelInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToSchedulerInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToStringCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToStringInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToT2IAdaptersInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToVAEInvocation" + }, + { + "$ref": "#/components/schemas/ModelIdentifierInvocation" + }, + { + "$ref": "#/components/schemas/MultiplyInvocation" + }, + { + "$ref": "#/components/schemas/NoiseInvocation" + }, + { + "$ref": "#/components/schemas/NormalMapInvocation" + }, + { + "$ref": "#/components/schemas/OklabUnsharpMaskInvocation" + }, + { + "$ref": "#/components/schemas/OklchImageHueAdjustmentInvocation" + }, + { + "$ref": "#/components/schemas/OpenAIImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/PBRMapsInvocation" + }, + { + "$ref": "#/components/schemas/PairTileImageInvocation" + }, + { + "$ref": "#/components/schemas/PasteImageIntoBoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/PiDiNetEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/PromptTemplateInvocation" + }, + { + "$ref": "#/components/schemas/PromptsFromFileInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageLatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/QwenImageLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/RandomFloatInvocation" + }, + { + "$ref": "#/components/schemas/RandomIntInvocation" + }, + { + "$ref": "#/components/schemas/RandomRangeInvocation" + }, + { + "$ref": "#/components/schemas/RangeInvocation" + }, + { + "$ref": "#/components/schemas/RangeOfSizeInvocation" + }, + { + "$ref": "#/components/schemas/RectangleMaskInvocation" + }, + { + "$ref": "#/components/schemas/ResizeLatentsInvocation" + }, + { + "$ref": "#/components/schemas/RoundInvocation" + }, + { + "$ref": "#/components/schemas/SD3DenoiseInvocation" + }, + { + "$ref": "#/components/schemas/SD3ImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/SD3LatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/SDXLCompelPromptInvocation" + }, + { + "$ref": "#/components/schemas/SDXLLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/SDXLLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/SDXLModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/SDXLRefinerCompelPromptInvocation" + }, + { + "$ref": "#/components/schemas/SDXLRefinerModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/SaveImageInvocation" + }, + { + "$ref": "#/components/schemas/SaveImageToFileInvocation" + }, + { + "$ref": "#/components/schemas/ScaleLatentsInvocation" + }, + { + "$ref": "#/components/schemas/SchedulerInvocation" + }, + { + "$ref": "#/components/schemas/Sd3ModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/Sd3TextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/SeamlessModeInvocation" + }, + { + "$ref": "#/components/schemas/SeedreamImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/SegmentAnythingInvocation" + }, + { + "$ref": "#/components/schemas/ShowImageInvocation" + }, + { + "$ref": "#/components/schemas/SpandrelImageToImageAutoscaleInvocation" + }, + { + "$ref": "#/components/schemas/SpandrelImageToImageInvocation" + }, + { + "$ref": "#/components/schemas/StringBatchInvocation" + }, + { + "$ref": "#/components/schemas/StringCollectionInvocation" + }, + { + "$ref": "#/components/schemas/StringGenerator" + }, + { + "$ref": "#/components/schemas/StringInvocation" + }, + { + "$ref": "#/components/schemas/StringJoinInvocation" + }, + { + "$ref": "#/components/schemas/StringJoinThreeInvocation" + }, + { + "$ref": "#/components/schemas/StringReplaceInvocation" + }, + { + "$ref": "#/components/schemas/StringSplitInvocation" + }, + { + "$ref": "#/components/schemas/StringSplitNegInvocation" + }, + { + "$ref": "#/components/schemas/SubtractInvocation" + }, + { + "$ref": "#/components/schemas/T2IAdapterInvocation" + }, + { + "$ref": "#/components/schemas/TextLLMInvocation" + }, + { + "$ref": "#/components/schemas/TileToPropertiesInvocation" + }, + { + "$ref": "#/components/schemas/TiledMultiDiffusionDenoiseLatents" + }, + { + "$ref": "#/components/schemas/UnsharpMaskInvocation" + }, + { + "$ref": "#/components/schemas/VAELoaderInvocation" + }, + { + "$ref": "#/components/schemas/ZImageControlInvocation" + }, + { + "$ref": "#/components/schemas/ZImageDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/ZImageDenoiseMetaInvocation" + }, + { + "$ref": "#/components/schemas/ZImageImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/ZImageLatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/ZImageLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/ZImageLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/ZImageModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/ZImageSeedVarianceEnhancerInvocation" + }, + { + "$ref": "#/components/schemas/ZImageTextEncoderInvocation" + } + ], + "title": "Invocation" + }, + "invocation_source_id": { + "description": "The ID of the prepared invocation's source node", + "title": "Invocation Source Id", + "type": "string" + }, + "result": { + "description": "The result of the invocation", + "oneOf": [ + { + "$ref": "#/components/schemas/AnimaConditioningOutput" + }, + { + "$ref": "#/components/schemas/AnimaLoRALoaderOutput" + }, + { + "$ref": "#/components/schemas/AnimaModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/BooleanCollectionOutput" + }, + { + "$ref": "#/components/schemas/BooleanOutput" + }, + { + "$ref": "#/components/schemas/BoundingBoxCollectionOutput" + }, + { + "$ref": "#/components/schemas/BoundingBoxOutput" + }, + { + "$ref": "#/components/schemas/CLIPOutput" + }, + { + "$ref": "#/components/schemas/CLIPSkipInvocationOutput" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesOutput" + }, + { + "$ref": "#/components/schemas/CogView4ConditioningOutput" + }, + { + "$ref": "#/components/schemas/CogView4ModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/CollectInvocationOutput" + }, + { + "$ref": "#/components/schemas/ColorCollectionOutput" + }, + { + "$ref": "#/components/schemas/ColorOutput" + }, + { + "$ref": "#/components/schemas/ConditioningCollectionOutput" + }, + { + "$ref": "#/components/schemas/ConditioningOutput" + }, + { + "$ref": "#/components/schemas/ControlOutput" + }, + { + "$ref": "#/components/schemas/DenoiseMaskOutput" + }, + { + "$ref": "#/components/schemas/FaceMaskOutput" + }, + { + "$ref": "#/components/schemas/FaceOffOutput" + }, + { + "$ref": "#/components/schemas/FloatCollectionOutput" + }, + { + "$ref": "#/components/schemas/FloatGeneratorOutput" + }, + { + "$ref": "#/components/schemas/FloatOutput" + }, + { + "$ref": "#/components/schemas/Flux2KleinLoRALoaderOutput" + }, + { + "$ref": "#/components/schemas/Flux2KleinModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/FluxConditioningCollectionOutput" + }, + { + "$ref": "#/components/schemas/FluxConditioningOutput" + }, + { + "$ref": "#/components/schemas/FluxControlLoRALoaderOutput" + }, + { + "$ref": "#/components/schemas/FluxControlNetOutput" + }, + { + "$ref": "#/components/schemas/FluxFillOutput" + }, + { + "$ref": "#/components/schemas/FluxKontextOutput" + }, + { + "$ref": "#/components/schemas/FluxLoRALoaderOutput" + }, + { + "$ref": "#/components/schemas/FluxModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/FluxReduxOutput" + }, + { + "$ref": "#/components/schemas/GradientMaskOutput" + }, + { + "$ref": "#/components/schemas/IPAdapterOutput" + }, + { + "$ref": "#/components/schemas/IdealSizeOutput" + }, + { + "$ref": "#/components/schemas/IfInvocationOutput" + }, + { + "$ref": "#/components/schemas/ImageCollectionOutput" + }, + { + "$ref": "#/components/schemas/ImageGeneratorOutput" + }, + { + "$ref": "#/components/schemas/ImageOutput" + }, + { + "$ref": "#/components/schemas/ImagePanelCoordinateOutput" + }, + { + "$ref": "#/components/schemas/IntegerCollectionOutput" + }, + { + "$ref": "#/components/schemas/IntegerGeneratorOutput" + }, + { + "$ref": "#/components/schemas/IntegerOutput" + }, + { + "$ref": "#/components/schemas/IterateInvocationOutput" + }, + { + "$ref": "#/components/schemas/LatentsCollectionOutput" + }, + { + "$ref": "#/components/schemas/LatentsMetaOutput" + }, + { + "$ref": "#/components/schemas/LatentsOutput" + }, + { + "$ref": "#/components/schemas/LoRALoaderOutput" + }, + { + "$ref": "#/components/schemas/LoRASelectorOutput" + }, + { + "$ref": "#/components/schemas/MDControlListOutput" + }, + { + "$ref": "#/components/schemas/MDIPAdapterListOutput" + }, + { + "$ref": "#/components/schemas/MDT2IAdapterListOutput" + }, + { + "$ref": "#/components/schemas/MaskOutput" + }, + { + "$ref": "#/components/schemas/MetadataItemOutput" + }, + { + "$ref": "#/components/schemas/MetadataOutput" + }, + { + "$ref": "#/components/schemas/MetadataToLorasCollectionOutput" + }, + { + "$ref": "#/components/schemas/MetadataToModelOutput" + }, + { + "$ref": "#/components/schemas/MetadataToSDXLModelOutput" + }, + { + "$ref": "#/components/schemas/ModelIdentifierOutput" + }, + { + "$ref": "#/components/schemas/ModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/NoiseOutput" + }, + { + "$ref": "#/components/schemas/PBRMapsOutput" + }, + { + "$ref": "#/components/schemas/PairTileImageOutput" + }, + { + "$ref": "#/components/schemas/PromptTemplateOutput" + }, + { + "$ref": "#/components/schemas/QwenImageConditioningOutput" + }, + { + "$ref": "#/components/schemas/QwenImageLoRALoaderOutput" + }, + { + "$ref": "#/components/schemas/QwenImageModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/SD3ConditioningOutput" + }, + { + "$ref": "#/components/schemas/SDXLLoRALoaderOutput" + }, + { + "$ref": "#/components/schemas/SDXLModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/SDXLRefinerModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/SchedulerOutput" + }, + { + "$ref": "#/components/schemas/Sd3ModelLoaderOutput" + }, + { + "$ref": "#/components/schemas/SeamlessModeOutput" + }, + { + "$ref": "#/components/schemas/String2Output" + }, + { + "$ref": "#/components/schemas/StringCollectionOutput" + }, + { + "$ref": "#/components/schemas/StringGeneratorOutput" + }, + { + "$ref": "#/components/schemas/StringOutput" + }, + { + "$ref": "#/components/schemas/StringPosNegOutput" + }, + { + "$ref": "#/components/schemas/T2IAdapterOutput" + }, + { + "$ref": "#/components/schemas/TileToPropertiesOutput" + }, + { + "$ref": "#/components/schemas/UNetOutput" + }, + { + "$ref": "#/components/schemas/VAEOutput" + }, + { + "$ref": "#/components/schemas/ZImageConditioningOutput" + }, + { + "$ref": "#/components/schemas/ZImageControlOutput" + }, + { + "$ref": "#/components/schemas/ZImageLoRALoaderOutput" + }, + { + "$ref": "#/components/schemas/ZImageModelLoaderOutput" + } + ], + "title": "Result" + } + }, + "required": [ + "timestamp", + "queue_id", + "item_id", + "batch_id", + "origin", + "destination", + "user_id", + "session_id", + "invocation", + "invocation_source_id", + "result" + ], + "title": "InvocationCompleteEvent", + "type": "object" + }, + "InvocationErrorEvent": { + "description": "Event model for invocation_error", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "queue_id": { + "description": "The ID of the queue", + "title": "Queue Id", + "type": "string" + }, + "item_id": { + "description": "The ID of the queue item", + "title": "Item Id", + "type": "integer" + }, + "batch_id": { + "description": "The ID of the queue batch", + "title": "Batch Id", + "type": "string" + }, + "origin": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The origin of the queue item", + "title": "Origin" + }, + "destination": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The destination of the queue item", + "title": "Destination" + }, + "user_id": { + "default": "system", + "description": "The ID of the user who created the queue item", + "title": "User Id", + "type": "string" + }, + "session_id": { + "description": "The ID of the session (aka graph execution state)", + "title": "Session Id", + "type": "string" + }, + "invocation": { + "description": "The ID of the invocation", + "oneOf": [ + { + "$ref": "#/components/schemas/AddInvocation" + }, + { + "$ref": "#/components/schemas/AlibabaCloudImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/AlphaMaskToTensorInvocation" + }, + { + "$ref": "#/components/schemas/AnimaDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/AnimaImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/AnimaLatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/AnimaLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/AnimaLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/AnimaModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/AnimaTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/ApplyMaskTensorToImageInvocation" + }, + { + "$ref": "#/components/schemas/ApplyMaskToImageInvocation" + }, + { + "$ref": "#/components/schemas/BlankImageInvocation" + }, + { + "$ref": "#/components/schemas/BlendLatentsInvocation" + }, + { + "$ref": "#/components/schemas/BooleanCollectionInvocation" + }, + { + "$ref": "#/components/schemas/BooleanInvocation" + }, + { + "$ref": "#/components/schemas/BoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/CLIPSkipInvocation" + }, + { + "$ref": "#/components/schemas/CV2InfillInvocation" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesEvenSplitInvocation" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesInvocation" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesMinimumOverlapInvocation" + }, + { + "$ref": "#/components/schemas/CannyEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/CanvasOutputInvocation" + }, + { + "$ref": "#/components/schemas/CanvasPasteBackInvocation" + }, + { + "$ref": "#/components/schemas/CanvasV2MaskAndCropInvocation" + }, + { + "$ref": "#/components/schemas/CenterPadCropInvocation" + }, + { + "$ref": "#/components/schemas/CogView4DenoiseInvocation" + }, + { + "$ref": "#/components/schemas/CogView4ImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/CogView4LatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/CogView4ModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/CogView4TextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/CollectInvocation" + }, + { + "$ref": "#/components/schemas/ColorCorrectInvocation" + }, + { + "$ref": "#/components/schemas/ColorInvocation" + }, + { + "$ref": "#/components/schemas/ColorMapInvocation" + }, + { + "$ref": "#/components/schemas/CompelInvocation" + }, + { + "$ref": "#/components/schemas/ConditioningCollectionInvocation" + }, + { + "$ref": "#/components/schemas/ConditioningInvocation" + }, + { + "$ref": "#/components/schemas/ContentShuffleInvocation" + }, + { + "$ref": "#/components/schemas/ControlNetInvocation" + }, + { + "$ref": "#/components/schemas/CoreMetadataInvocation" + }, + { + "$ref": "#/components/schemas/CreateDenoiseMaskInvocation" + }, + { + "$ref": "#/components/schemas/CreateGradientMaskInvocation" + }, + { + "$ref": "#/components/schemas/CropImageToBoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/CropLatentsCoreInvocation" + }, + { + "$ref": "#/components/schemas/CvInpaintInvocation" + }, + { + "$ref": "#/components/schemas/DWOpenposeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/DecodeInvisibleWatermarkInvocation" + }, + { + "$ref": "#/components/schemas/DenoiseLatentsInvocation" + }, + { + "$ref": "#/components/schemas/DenoiseLatentsMetaInvocation" + }, + { + "$ref": "#/components/schemas/DepthAnythingDepthEstimationInvocation" + }, + { + "$ref": "#/components/schemas/DivideInvocation" + }, + { + "$ref": "#/components/schemas/DynamicPromptInvocation" + }, + { + "$ref": "#/components/schemas/ESRGANInvocation" + }, + { + "$ref": "#/components/schemas/ExpandMaskWithFadeInvocation" + }, + { + "$ref": "#/components/schemas/FLUXLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/FaceIdentifierInvocation" + }, + { + "$ref": "#/components/schemas/FaceMaskInvocation" + }, + { + "$ref": "#/components/schemas/FaceOffInvocation" + }, + { + "$ref": "#/components/schemas/FloatBatchInvocation" + }, + { + "$ref": "#/components/schemas/FloatCollectionInvocation" + }, + { + "$ref": "#/components/schemas/FloatGenerator" + }, + { + "$ref": "#/components/schemas/FloatInvocation" + }, + { + "$ref": "#/components/schemas/FloatLinearRangeInvocation" + }, + { + "$ref": "#/components/schemas/FloatMathInvocation" + }, + { + "$ref": "#/components/schemas/FloatToIntegerInvocation" + }, + { + "$ref": "#/components/schemas/Flux2DenoiseInvocation" + }, + { + "$ref": "#/components/schemas/Flux2KleinLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/Flux2KleinLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/Flux2KleinModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/Flux2KleinTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/Flux2VaeDecodeInvocation" + }, + { + "$ref": "#/components/schemas/Flux2VaeEncodeInvocation" + }, + { + "$ref": "#/components/schemas/FluxControlLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/FluxControlNetInvocation" + }, + { + "$ref": "#/components/schemas/FluxDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/FluxDenoiseLatentsMetaInvocation" + }, + { + "$ref": "#/components/schemas/FluxFillInvocation" + }, + { + "$ref": "#/components/schemas/FluxIPAdapterInvocation" + }, + { + "$ref": "#/components/schemas/FluxKontextConcatenateImagesInvocation" + }, + { + "$ref": "#/components/schemas/FluxKontextInvocation" + }, + { + "$ref": "#/components/schemas/FluxLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/FluxModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/FluxReduxInvocation" + }, + { + "$ref": "#/components/schemas/FluxTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/FluxVaeDecodeInvocation" + }, + { + "$ref": "#/components/schemas/FluxVaeEncodeInvocation" + }, + { + "$ref": "#/components/schemas/FreeUInvocation" + }, + { + "$ref": "#/components/schemas/GeminiImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/GetMaskBoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/GroundingDinoInvocation" + }, + { + "$ref": "#/components/schemas/HEDEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/HeuristicResizeInvocation" + }, + { + "$ref": "#/components/schemas/IPAdapterInvocation" + }, + { + "$ref": "#/components/schemas/IdealSizeInvocation" + }, + { + "$ref": "#/components/schemas/IfInvocation" + }, + { + "$ref": "#/components/schemas/ImageBatchInvocation" + }, + { + "$ref": "#/components/schemas/ImageBlurInvocation" + }, + { + "$ref": "#/components/schemas/ImageChannelInvocation" + }, + { + "$ref": "#/components/schemas/ImageChannelMultiplyInvocation" + }, + { + "$ref": "#/components/schemas/ImageChannelOffsetInvocation" + }, + { + "$ref": "#/components/schemas/ImageCollectionInvocation" + }, + { + "$ref": "#/components/schemas/ImageConvertInvocation" + }, + { + "$ref": "#/components/schemas/ImageCropInvocation" + }, + { + "$ref": "#/components/schemas/ImageGenerator" + }, + { + "$ref": "#/components/schemas/ImageHueAdjustmentInvocation" + }, + { + "$ref": "#/components/schemas/ImageInverseLerpInvocation" + }, + { + "$ref": "#/components/schemas/ImageInvocation" + }, + { + "$ref": "#/components/schemas/ImageLerpInvocation" + }, + { + "$ref": "#/components/schemas/ImageMaskToTensorInvocation" + }, + { + "$ref": "#/components/schemas/ImageMultiplyInvocation" + }, + { + "$ref": "#/components/schemas/ImageNSFWBlurInvocation" + }, + { + "$ref": "#/components/schemas/ImageNoiseInvocation" + }, + { + "$ref": "#/components/schemas/ImagePanelLayoutInvocation" + }, + { + "$ref": "#/components/schemas/ImagePasteInvocation" + }, + { + "$ref": "#/components/schemas/ImageResizeInvocation" + }, + { + "$ref": "#/components/schemas/ImageScaleInvocation" + }, + { + "$ref": "#/components/schemas/ImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/ImageWatermarkInvocation" + }, + { + "$ref": "#/components/schemas/InfillColorInvocation" + }, + { + "$ref": "#/components/schemas/InfillPatchMatchInvocation" + }, + { + "$ref": "#/components/schemas/InfillTileInvocation" + }, + { + "$ref": "#/components/schemas/IntegerBatchInvocation" + }, + { + "$ref": "#/components/schemas/IntegerCollectionInvocation" + }, + { + "$ref": "#/components/schemas/IntegerGenerator" + }, + { + "$ref": "#/components/schemas/IntegerInvocation" + }, + { + "$ref": "#/components/schemas/IntegerMathInvocation" + }, + { + "$ref": "#/components/schemas/InvertTensorMaskInvocation" + }, + { + "$ref": "#/components/schemas/InvokeAdjustImageHuePlusInvocation" + }, + { + "$ref": "#/components/schemas/InvokeEquivalentAchromaticLightnessInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageBlendInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageCompositorInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageDilateOrErodeInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageEnhanceInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageValueThresholdsInvocation" + }, + { + "$ref": "#/components/schemas/IterateInvocation" + }, + { + "$ref": "#/components/schemas/LaMaInfillInvocation" + }, + { + "$ref": "#/components/schemas/LatentsCollectionInvocation" + }, + { + "$ref": "#/components/schemas/LatentsInvocation" + }, + { + "$ref": "#/components/schemas/LatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/LineartAnimeEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/LineartEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/LlavaOnevisionVllmInvocation" + }, + { + "$ref": "#/components/schemas/LoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/LoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/LoRASelectorInvocation" + }, + { + "$ref": "#/components/schemas/MLSDDetectionInvocation" + }, + { + "$ref": "#/components/schemas/MainModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/MaskCombineInvocation" + }, + { + "$ref": "#/components/schemas/MaskEdgeInvocation" + }, + { + "$ref": "#/components/schemas/MaskFromAlphaInvocation" + }, + { + "$ref": "#/components/schemas/MaskFromIDInvocation" + }, + { + "$ref": "#/components/schemas/MaskTensorToImageInvocation" + }, + { + "$ref": "#/components/schemas/MediaPipeFaceDetectionInvocation" + }, + { + "$ref": "#/components/schemas/MergeMetadataInvocation" + }, + { + "$ref": "#/components/schemas/MergeTilesToImageInvocation" + }, + { + "$ref": "#/components/schemas/MetadataFieldExtractorInvocation" + }, + { + "$ref": "#/components/schemas/MetadataFromImageInvocation" + }, + { + "$ref": "#/components/schemas/MetadataInvocation" + }, + { + "$ref": "#/components/schemas/MetadataItemInvocation" + }, + { + "$ref": "#/components/schemas/MetadataItemLinkedInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToBoolCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToBoolInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToControlnetsInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToFloatCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToFloatInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToIPAdaptersInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToIntegerCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToIntegerInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToLorasCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToLorasInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToModelInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToSDXLLorasInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToSDXLModelInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToSchedulerInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToStringCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToStringInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToT2IAdaptersInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToVAEInvocation" + }, + { + "$ref": "#/components/schemas/ModelIdentifierInvocation" + }, + { + "$ref": "#/components/schemas/MultiplyInvocation" + }, + { + "$ref": "#/components/schemas/NoiseInvocation" + }, + { + "$ref": "#/components/schemas/NormalMapInvocation" + }, + { + "$ref": "#/components/schemas/OklabUnsharpMaskInvocation" + }, + { + "$ref": "#/components/schemas/OklchImageHueAdjustmentInvocation" + }, + { + "$ref": "#/components/schemas/OpenAIImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/PBRMapsInvocation" + }, + { + "$ref": "#/components/schemas/PairTileImageInvocation" + }, + { + "$ref": "#/components/schemas/PasteImageIntoBoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/PiDiNetEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/PromptTemplateInvocation" + }, + { + "$ref": "#/components/schemas/PromptsFromFileInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageLatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/QwenImageLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/RandomFloatInvocation" + }, + { + "$ref": "#/components/schemas/RandomIntInvocation" + }, + { + "$ref": "#/components/schemas/RandomRangeInvocation" + }, + { + "$ref": "#/components/schemas/RangeInvocation" + }, + { + "$ref": "#/components/schemas/RangeOfSizeInvocation" + }, + { + "$ref": "#/components/schemas/RectangleMaskInvocation" + }, + { + "$ref": "#/components/schemas/ResizeLatentsInvocation" + }, + { + "$ref": "#/components/schemas/RoundInvocation" + }, + { + "$ref": "#/components/schemas/SD3DenoiseInvocation" + }, + { + "$ref": "#/components/schemas/SD3ImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/SD3LatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/SDXLCompelPromptInvocation" + }, + { + "$ref": "#/components/schemas/SDXLLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/SDXLLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/SDXLModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/SDXLRefinerCompelPromptInvocation" + }, + { + "$ref": "#/components/schemas/SDXLRefinerModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/SaveImageInvocation" + }, + { + "$ref": "#/components/schemas/SaveImageToFileInvocation" + }, + { + "$ref": "#/components/schemas/ScaleLatentsInvocation" + }, + { + "$ref": "#/components/schemas/SchedulerInvocation" + }, + { + "$ref": "#/components/schemas/Sd3ModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/Sd3TextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/SeamlessModeInvocation" + }, + { + "$ref": "#/components/schemas/SeedreamImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/SegmentAnythingInvocation" + }, + { + "$ref": "#/components/schemas/ShowImageInvocation" + }, + { + "$ref": "#/components/schemas/SpandrelImageToImageAutoscaleInvocation" + }, + { + "$ref": "#/components/schemas/SpandrelImageToImageInvocation" + }, + { + "$ref": "#/components/schemas/StringBatchInvocation" + }, + { + "$ref": "#/components/schemas/StringCollectionInvocation" + }, + { + "$ref": "#/components/schemas/StringGenerator" + }, + { + "$ref": "#/components/schemas/StringInvocation" + }, + { + "$ref": "#/components/schemas/StringJoinInvocation" + }, + { + "$ref": "#/components/schemas/StringJoinThreeInvocation" + }, + { + "$ref": "#/components/schemas/StringReplaceInvocation" + }, + { + "$ref": "#/components/schemas/StringSplitInvocation" + }, + { + "$ref": "#/components/schemas/StringSplitNegInvocation" + }, + { + "$ref": "#/components/schemas/SubtractInvocation" + }, + { + "$ref": "#/components/schemas/T2IAdapterInvocation" + }, + { + "$ref": "#/components/schemas/TextLLMInvocation" + }, + { + "$ref": "#/components/schemas/TileToPropertiesInvocation" + }, + { + "$ref": "#/components/schemas/TiledMultiDiffusionDenoiseLatents" + }, + { + "$ref": "#/components/schemas/UnsharpMaskInvocation" + }, + { + "$ref": "#/components/schemas/VAELoaderInvocation" + }, + { + "$ref": "#/components/schemas/ZImageControlInvocation" + }, + { + "$ref": "#/components/schemas/ZImageDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/ZImageDenoiseMetaInvocation" + }, + { + "$ref": "#/components/schemas/ZImageImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/ZImageLatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/ZImageLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/ZImageLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/ZImageModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/ZImageSeedVarianceEnhancerInvocation" + }, + { + "$ref": "#/components/schemas/ZImageTextEncoderInvocation" + } + ], + "title": "Invocation" + }, + "invocation_source_id": { + "description": "The ID of the prepared invocation's source node", + "title": "Invocation Source Id", + "type": "string" + }, + "error_type": { + "description": "The error type", + "title": "Error Type", + "type": "string" + }, + "error_message": { + "description": "The error message", + "title": "Error Message", + "type": "string" + }, + "error_traceback": { + "description": "The error traceback", + "title": "Error Traceback", + "type": "string" + } + }, + "required": [ + "timestamp", + "queue_id", + "item_id", + "batch_id", + "origin", + "destination", + "user_id", + "session_id", + "invocation", + "invocation_source_id", + "error_type", + "error_message", + "error_traceback" + ], + "title": "InvocationErrorEvent", + "type": "object" + }, + "InvocationOutputMap": { + "type": "object", + "properties": { + "add": { + "$ref": "#/components/schemas/IntegerOutput" + }, + "alibabacloud_image_generation": { + "$ref": "#/components/schemas/ImageCollectionOutput" + }, + "alpha_mask_to_tensor": { + "$ref": "#/components/schemas/MaskOutput" + }, + "anima_denoise": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "anima_i2l": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "anima_l2i": { + "$ref": "#/components/schemas/ImageOutput" + }, + "anima_lora_collection_loader": { + "$ref": "#/components/schemas/AnimaLoRALoaderOutput" + }, + "anima_lora_loader": { + "$ref": "#/components/schemas/AnimaLoRALoaderOutput" + }, + "anima_model_loader": { + "$ref": "#/components/schemas/AnimaModelLoaderOutput" + }, + "anima_text_encoder": { + "$ref": "#/components/schemas/AnimaConditioningOutput" + }, + "apply_mask_to_image": { + "$ref": "#/components/schemas/ImageOutput" + }, + "apply_tensor_mask_to_image": { + "$ref": "#/components/schemas/ImageOutput" + }, + "blank_image": { + "$ref": "#/components/schemas/ImageOutput" + }, + "boolean": { + "$ref": "#/components/schemas/BooleanOutput" + }, + "boolean_collection": { + "$ref": "#/components/schemas/BooleanCollectionOutput" + }, + "bounding_box": { + "$ref": "#/components/schemas/BoundingBoxOutput" + }, + "calculate_image_tiles": { + "$ref": "#/components/schemas/CalculateImageTilesOutput" + }, + "calculate_image_tiles_even_split": { + "$ref": "#/components/schemas/CalculateImageTilesOutput" + }, + "calculate_image_tiles_min_overlap": { + "$ref": "#/components/schemas/CalculateImageTilesOutput" + }, + "canny_edge_detection": { + "$ref": "#/components/schemas/ImageOutput" + }, + "canvas_output": { + "$ref": "#/components/schemas/ImageOutput" + }, + "canvas_paste_back": { + "$ref": "#/components/schemas/ImageOutput" + }, + "canvas_v2_mask_and_crop": { + "$ref": "#/components/schemas/ImageOutput" + }, + "clip_skip": { + "$ref": "#/components/schemas/CLIPSkipInvocationOutput" + }, + "cogview4_denoise": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "cogview4_i2l": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "cogview4_l2i": { + "$ref": "#/components/schemas/ImageOutput" + }, + "cogview4_model_loader": { + "$ref": "#/components/schemas/CogView4ModelLoaderOutput" + }, + "cogview4_text_encoder": { + "$ref": "#/components/schemas/CogView4ConditioningOutput" + }, + "collect": { + "$ref": "#/components/schemas/CollectInvocationOutput" + }, + "color": { + "$ref": "#/components/schemas/ColorOutput" + }, + "color_correct": { + "$ref": "#/components/schemas/ImageOutput" + }, + "color_map": { + "$ref": "#/components/schemas/ImageOutput" + }, + "compel": { + "$ref": "#/components/schemas/ConditioningOutput" + }, + "conditioning": { + "$ref": "#/components/schemas/ConditioningOutput" + }, + "conditioning_collection": { + "$ref": "#/components/schemas/ConditioningCollectionOutput" + }, + "content_shuffle": { + "$ref": "#/components/schemas/ImageOutput" + }, + "controlnet": { + "$ref": "#/components/schemas/ControlOutput" + }, + "core_metadata": { + "$ref": "#/components/schemas/MetadataOutput" + }, + "create_denoise_mask": { + "$ref": "#/components/schemas/DenoiseMaskOutput" + }, + "create_gradient_mask": { + "$ref": "#/components/schemas/GradientMaskOutput" + }, + "crop_image_to_bounding_box": { + "$ref": "#/components/schemas/ImageOutput" + }, + "crop_latents": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "cv_inpaint": { + "$ref": "#/components/schemas/ImageOutput" + }, + "decode_watermark": { + "$ref": "#/components/schemas/StringOutput" + }, + "denoise_latents": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "denoise_latents_meta": { + "$ref": "#/components/schemas/LatentsMetaOutput" + }, + "depth_anything_depth_estimation": { + "$ref": "#/components/schemas/ImageOutput" + }, + "div": { + "$ref": "#/components/schemas/IntegerOutput" + }, + "dw_openpose_detection": { + "$ref": "#/components/schemas/ImageOutput" + }, + "dynamic_prompt": { + "$ref": "#/components/schemas/StringCollectionOutput" + }, + "esrgan": { + "$ref": "#/components/schemas/ImageOutput" + }, + "expand_mask_with_fade": { + "$ref": "#/components/schemas/ImageOutput" + }, + "face_identifier": { + "$ref": "#/components/schemas/ImageOutput" + }, + "face_mask_detection": { + "$ref": "#/components/schemas/FaceMaskOutput" + }, + "face_off": { + "$ref": "#/components/schemas/FaceOffOutput" + }, + "float": { + "$ref": "#/components/schemas/FloatOutput" + }, + "float_batch": { + "$ref": "#/components/schemas/FloatOutput" + }, + "float_collection": { + "$ref": "#/components/schemas/FloatCollectionOutput" + }, + "float_generator": { + "$ref": "#/components/schemas/FloatGeneratorOutput" + }, + "float_math": { + "$ref": "#/components/schemas/FloatOutput" + }, + "float_range": { + "$ref": "#/components/schemas/FloatCollectionOutput" + }, + "float_to_int": { + "$ref": "#/components/schemas/IntegerOutput" + }, + "flux2_denoise": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "flux2_klein_lora_collection_loader": { + "$ref": "#/components/schemas/Flux2KleinLoRALoaderOutput" + }, + "flux2_klein_lora_loader": { + "$ref": "#/components/schemas/Flux2KleinLoRALoaderOutput" + }, + "flux2_klein_model_loader": { + "$ref": "#/components/schemas/Flux2KleinModelLoaderOutput" + }, + "flux2_klein_text_encoder": { + "$ref": "#/components/schemas/FluxConditioningOutput" + }, + "flux2_vae_decode": { + "$ref": "#/components/schemas/ImageOutput" + }, + "flux2_vae_encode": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "flux_control_lora_loader": { + "$ref": "#/components/schemas/FluxControlLoRALoaderOutput" + }, + "flux_controlnet": { + "$ref": "#/components/schemas/FluxControlNetOutput" + }, + "flux_denoise": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "flux_denoise_meta": { + "$ref": "#/components/schemas/LatentsMetaOutput" + }, + "flux_fill": { + "$ref": "#/components/schemas/FluxFillOutput" + }, + "flux_ip_adapter": { + "$ref": "#/components/schemas/IPAdapterOutput" + }, + "flux_kontext": { + "$ref": "#/components/schemas/FluxKontextOutput" + }, + "flux_kontext_image_prep": { + "$ref": "#/components/schemas/ImageOutput" + }, + "flux_lora_collection_loader": { + "$ref": "#/components/schemas/FluxLoRALoaderOutput" + }, + "flux_lora_loader": { + "$ref": "#/components/schemas/FluxLoRALoaderOutput" + }, + "flux_model_loader": { + "$ref": "#/components/schemas/FluxModelLoaderOutput" + }, + "flux_redux": { + "$ref": "#/components/schemas/FluxReduxOutput" + }, + "flux_text_encoder": { + "$ref": "#/components/schemas/FluxConditioningOutput" + }, + "flux_vae_decode": { + "$ref": "#/components/schemas/ImageOutput" + }, + "flux_vae_encode": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "freeu": { + "$ref": "#/components/schemas/UNetOutput" + }, + "gemini_image_generation": { + "$ref": "#/components/schemas/ImageCollectionOutput" + }, + "get_image_mask_bounding_box": { + "$ref": "#/components/schemas/BoundingBoxOutput" + }, + "grounding_dino": { + "$ref": "#/components/schemas/BoundingBoxCollectionOutput" + }, + "hed_edge_detection": { + "$ref": "#/components/schemas/ImageOutput" + }, + "heuristic_resize": { + "$ref": "#/components/schemas/ImageOutput" + }, + "i2l": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "ideal_size": { + "$ref": "#/components/schemas/IdealSizeOutput" + }, + "if": { + "$ref": "#/components/schemas/IfInvocationOutput" + }, + "image": { + "$ref": "#/components/schemas/ImageOutput" + }, + "image_batch": { + "$ref": "#/components/schemas/ImageOutput" + }, + "image_collection": { + "$ref": "#/components/schemas/ImageCollectionOutput" + }, + "image_generator": { + "$ref": "#/components/schemas/ImageGeneratorOutput" + }, + "image_mask_to_tensor": { + "$ref": "#/components/schemas/MaskOutput" + }, + "image_panel_layout": { + "$ref": "#/components/schemas/ImagePanelCoordinateOutput" + }, + "img_blur": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_chan": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_channel_multiply": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_channel_offset": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_conv": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_crop": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_hue_adjust": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_hue_adjust_oklch": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_ilerp": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_lerp": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_mul": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_noise": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_nsfw": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_pad_crop": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_paste": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_resize": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_scale": { + "$ref": "#/components/schemas/ImageOutput" + }, + "img_watermark": { + "$ref": "#/components/schemas/ImageOutput" + }, + "infill_cv2": { + "$ref": "#/components/schemas/ImageOutput" + }, + "infill_lama": { + "$ref": "#/components/schemas/ImageOutput" + }, + "infill_patchmatch": { + "$ref": "#/components/schemas/ImageOutput" + }, + "infill_rgba": { + "$ref": "#/components/schemas/ImageOutput" + }, + "infill_tile": { + "$ref": "#/components/schemas/ImageOutput" + }, + "integer": { + "$ref": "#/components/schemas/IntegerOutput" + }, + "integer_batch": { + "$ref": "#/components/schemas/IntegerOutput" + }, + "integer_collection": { + "$ref": "#/components/schemas/IntegerCollectionOutput" + }, + "integer_generator": { + "$ref": "#/components/schemas/IntegerGeneratorOutput" + }, + "integer_math": { + "$ref": "#/components/schemas/IntegerOutput" + }, + "invert_tensor_mask": { + "$ref": "#/components/schemas/MaskOutput" + }, + "invokeai_ealightness": { + "$ref": "#/components/schemas/ImageOutput" + }, + "invokeai_img_blend": { + "$ref": "#/components/schemas/ImageOutput" + }, + "invokeai_img_composite": { + "$ref": "#/components/schemas/ImageOutput" + }, + "invokeai_img_dilate_erode": { + "$ref": "#/components/schemas/ImageOutput" + }, + "invokeai_img_enhance": { + "$ref": "#/components/schemas/ImageOutput" + }, + "invokeai_img_hue_adjust_plus": { + "$ref": "#/components/schemas/ImageOutput" + }, + "invokeai_img_val_thresholds": { + "$ref": "#/components/schemas/ImageOutput" + }, + "ip_adapter": { + "$ref": "#/components/schemas/IPAdapterOutput" + }, + "iterate": { + "$ref": "#/components/schemas/IterateInvocationOutput" + }, + "l2i": { + "$ref": "#/components/schemas/ImageOutput" + }, + "latents": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "latents_collection": { + "$ref": "#/components/schemas/LatentsCollectionOutput" + }, + "lblend": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "lineart_anime_edge_detection": { + "$ref": "#/components/schemas/ImageOutput" + }, + "lineart_edge_detection": { + "$ref": "#/components/schemas/ImageOutput" + }, + "llava_onevision_vllm": { + "$ref": "#/components/schemas/StringOutput" + }, + "lora_collection_loader": { + "$ref": "#/components/schemas/LoRALoaderOutput" + }, + "lora_loader": { + "$ref": "#/components/schemas/LoRALoaderOutput" + }, + "lora_selector": { + "$ref": "#/components/schemas/LoRASelectorOutput" + }, + "lresize": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "lscale": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "main_model_loader": { + "$ref": "#/components/schemas/ModelLoaderOutput" + }, + "mask_combine": { + "$ref": "#/components/schemas/ImageOutput" + }, + "mask_edge": { + "$ref": "#/components/schemas/ImageOutput" + }, + "mask_from_id": { + "$ref": "#/components/schemas/ImageOutput" + }, + "mediapipe_face_detection": { + "$ref": "#/components/schemas/ImageOutput" + }, + "merge_metadata": { + "$ref": "#/components/schemas/MetadataOutput" + }, + "merge_tiles_to_image": { + "$ref": "#/components/schemas/ImageOutput" + }, + "metadata": { + "$ref": "#/components/schemas/MetadataOutput" + }, + "metadata_field_extractor": { + "$ref": "#/components/schemas/StringOutput" + }, + "metadata_from_image": { + "$ref": "#/components/schemas/MetadataOutput" + }, + "metadata_item": { + "$ref": "#/components/schemas/MetadataItemOutput" + }, + "metadata_item_linked": { + "$ref": "#/components/schemas/MetadataOutput" + }, + "metadata_to_bool": { + "$ref": "#/components/schemas/BooleanOutput" + }, + "metadata_to_bool_collection": { + "$ref": "#/components/schemas/BooleanCollectionOutput" + }, + "metadata_to_controlnets": { + "$ref": "#/components/schemas/MDControlListOutput" + }, + "metadata_to_float": { + "$ref": "#/components/schemas/FloatOutput" + }, + "metadata_to_float_collection": { + "$ref": "#/components/schemas/FloatCollectionOutput" + }, + "metadata_to_integer": { + "$ref": "#/components/schemas/IntegerOutput" + }, + "metadata_to_integer_collection": { + "$ref": "#/components/schemas/IntegerCollectionOutput" + }, + "metadata_to_ip_adapters": { + "$ref": "#/components/schemas/MDIPAdapterListOutput" + }, + "metadata_to_lora_collection": { + "$ref": "#/components/schemas/MetadataToLorasCollectionOutput" + }, + "metadata_to_loras": { + "$ref": "#/components/schemas/LoRALoaderOutput" + }, + "metadata_to_model": { + "$ref": "#/components/schemas/MetadataToModelOutput" + }, + "metadata_to_scheduler": { + "$ref": "#/components/schemas/SchedulerOutput" + }, + "metadata_to_sdlx_loras": { + "$ref": "#/components/schemas/SDXLLoRALoaderOutput" + }, + "metadata_to_sdxl_model": { + "$ref": "#/components/schemas/MetadataToSDXLModelOutput" + }, + "metadata_to_string": { + "$ref": "#/components/schemas/StringOutput" + }, + "metadata_to_string_collection": { + "$ref": "#/components/schemas/StringCollectionOutput" + }, + "metadata_to_t2i_adapters": { + "$ref": "#/components/schemas/MDT2IAdapterListOutput" + }, + "metadata_to_vae": { + "$ref": "#/components/schemas/VAEOutput" + }, + "mlsd_detection": { + "$ref": "#/components/schemas/ImageOutput" + }, + "model_identifier": { + "$ref": "#/components/schemas/ModelIdentifierOutput" + }, + "mul": { + "$ref": "#/components/schemas/IntegerOutput" + }, + "noise": { + "$ref": "#/components/schemas/NoiseOutput" + }, + "normal_map": { + "$ref": "#/components/schemas/ImageOutput" + }, + "openai_image_generation": { + "$ref": "#/components/schemas/ImageCollectionOutput" + }, + "pair_tile_image": { + "$ref": "#/components/schemas/PairTileImageOutput" + }, + "paste_image_into_bounding_box": { + "$ref": "#/components/schemas/ImageOutput" + }, + "pbr_maps": { + "$ref": "#/components/schemas/PBRMapsOutput" + }, + "pidi_edge_detection": { + "$ref": "#/components/schemas/ImageOutput" + }, + "prompt_from_file": { + "$ref": "#/components/schemas/StringCollectionOutput" + }, + "prompt_template": { + "$ref": "#/components/schemas/PromptTemplateOutput" + }, + "qwen_image_denoise": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "qwen_image_i2l": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "qwen_image_l2i": { + "$ref": "#/components/schemas/ImageOutput" + }, + "qwen_image_lora_collection_loader": { + "$ref": "#/components/schemas/QwenImageLoRALoaderOutput" + }, + "qwen_image_lora_loader": { + "$ref": "#/components/schemas/QwenImageLoRALoaderOutput" + }, + "qwen_image_model_loader": { + "$ref": "#/components/schemas/QwenImageModelLoaderOutput" + }, + "qwen_image_text_encoder": { + "$ref": "#/components/schemas/QwenImageConditioningOutput" + }, + "rand_float": { + "$ref": "#/components/schemas/FloatOutput" + }, + "rand_int": { + "$ref": "#/components/schemas/IntegerOutput" + }, + "random_range": { + "$ref": "#/components/schemas/IntegerCollectionOutput" + }, + "range": { + "$ref": "#/components/schemas/IntegerCollectionOutput" + }, + "range_of_size": { + "$ref": "#/components/schemas/IntegerCollectionOutput" + }, + "rectangle_mask": { + "$ref": "#/components/schemas/MaskOutput" + }, + "round_float": { + "$ref": "#/components/schemas/FloatOutput" + }, + "save_image": { + "$ref": "#/components/schemas/ImageOutput" + }, + "save_image_to_file": { + "$ref": "#/components/schemas/ImageOutput" + }, + "scheduler": { + "$ref": "#/components/schemas/SchedulerOutput" + }, + "sd3_denoise": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "sd3_i2l": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "sd3_l2i": { + "$ref": "#/components/schemas/ImageOutput" + }, + "sd3_model_loader": { + "$ref": "#/components/schemas/Sd3ModelLoaderOutput" + }, + "sd3_text_encoder": { + "$ref": "#/components/schemas/SD3ConditioningOutput" + }, + "sdxl_compel_prompt": { + "$ref": "#/components/schemas/ConditioningOutput" + }, + "sdxl_lora_collection_loader": { + "$ref": "#/components/schemas/SDXLLoRALoaderOutput" + }, + "sdxl_lora_loader": { + "$ref": "#/components/schemas/SDXLLoRALoaderOutput" + }, + "sdxl_model_loader": { + "$ref": "#/components/schemas/SDXLModelLoaderOutput" + }, + "sdxl_refiner_compel_prompt": { + "$ref": "#/components/schemas/ConditioningOutput" + }, + "sdxl_refiner_model_loader": { + "$ref": "#/components/schemas/SDXLRefinerModelLoaderOutput" + }, + "seamless": { + "$ref": "#/components/schemas/SeamlessModeOutput" + }, + "seedream_image_generation": { + "$ref": "#/components/schemas/ImageCollectionOutput" + }, + "segment_anything": { + "$ref": "#/components/schemas/MaskOutput" + }, + "show_image": { + "$ref": "#/components/schemas/ImageOutput" + }, + "spandrel_image_to_image": { + "$ref": "#/components/schemas/ImageOutput" + }, + "spandrel_image_to_image_autoscale": { + "$ref": "#/components/schemas/ImageOutput" + }, + "string": { + "$ref": "#/components/schemas/StringOutput" + }, + "string_batch": { + "$ref": "#/components/schemas/StringOutput" + }, + "string_collection": { + "$ref": "#/components/schemas/StringCollectionOutput" + }, + "string_generator": { + "$ref": "#/components/schemas/StringGeneratorOutput" + }, + "string_join": { + "$ref": "#/components/schemas/StringOutput" + }, + "string_join_three": { + "$ref": "#/components/schemas/StringOutput" + }, + "string_replace": { + "$ref": "#/components/schemas/StringOutput" + }, + "string_split": { + "$ref": "#/components/schemas/String2Output" + }, + "string_split_neg": { + "$ref": "#/components/schemas/StringPosNegOutput" + }, + "sub": { + "$ref": "#/components/schemas/IntegerOutput" + }, + "t2i_adapter": { + "$ref": "#/components/schemas/T2IAdapterOutput" + }, + "tensor_mask_to_image": { + "$ref": "#/components/schemas/ImageOutput" + }, + "text_llm": { + "$ref": "#/components/schemas/StringOutput" + }, + "tile_to_properties": { + "$ref": "#/components/schemas/TileToPropertiesOutput" + }, + "tiled_multi_diffusion_denoise_latents": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "tomask": { + "$ref": "#/components/schemas/ImageOutput" + }, + "unsharp_mask": { + "$ref": "#/components/schemas/ImageOutput" + }, + "unsharp_mask_oklab": { + "$ref": "#/components/schemas/ImageOutput" + }, + "vae_loader": { + "$ref": "#/components/schemas/VAEOutput" + }, + "z_image_control": { + "$ref": "#/components/schemas/ZImageControlOutput" + }, + "z_image_denoise": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "z_image_denoise_meta": { + "$ref": "#/components/schemas/LatentsMetaOutput" + }, + "z_image_i2l": { + "$ref": "#/components/schemas/LatentsOutput" + }, + "z_image_l2i": { + "$ref": "#/components/schemas/ImageOutput" + }, + "z_image_lora_collection_loader": { + "$ref": "#/components/schemas/ZImageLoRALoaderOutput" + }, + "z_image_lora_loader": { + "$ref": "#/components/schemas/ZImageLoRALoaderOutput" + }, + "z_image_model_loader": { + "$ref": "#/components/schemas/ZImageModelLoaderOutput" + }, + "z_image_seed_variance_enhancer": { + "$ref": "#/components/schemas/ZImageConditioningOutput" + }, + "z_image_text_encoder": { + "$ref": "#/components/schemas/ZImageConditioningOutput" + } + }, + "required": [ + "add", + "alibabacloud_image_generation", + "alpha_mask_to_tensor", + "anima_denoise", + "anima_i2l", + "anima_l2i", + "anima_lora_collection_loader", + "anima_lora_loader", + "anima_model_loader", + "anima_text_encoder", + "apply_mask_to_image", + "apply_tensor_mask_to_image", + "blank_image", + "boolean", + "boolean_collection", + "bounding_box", + "calculate_image_tiles", + "calculate_image_tiles_even_split", + "calculate_image_tiles_min_overlap", + "canny_edge_detection", + "canvas_output", + "canvas_paste_back", + "canvas_v2_mask_and_crop", + "clip_skip", + "cogview4_denoise", + "cogview4_i2l", + "cogview4_l2i", + "cogview4_model_loader", + "cogview4_text_encoder", + "collect", + "color", + "color_correct", + "color_map", + "compel", + "conditioning", + "conditioning_collection", + "content_shuffle", + "controlnet", + "core_metadata", + "create_denoise_mask", + "create_gradient_mask", + "crop_image_to_bounding_box", + "crop_latents", + "cv_inpaint", + "decode_watermark", + "denoise_latents", + "denoise_latents_meta", + "depth_anything_depth_estimation", + "div", + "dw_openpose_detection", + "dynamic_prompt", + "esrgan", + "expand_mask_with_fade", + "face_identifier", + "face_mask_detection", + "face_off", + "float", + "float_batch", + "float_collection", + "float_generator", + "float_math", + "float_range", + "float_to_int", + "flux2_denoise", + "flux2_klein_lora_collection_loader", + "flux2_klein_lora_loader", + "flux2_klein_model_loader", + "flux2_klein_text_encoder", + "flux2_vae_decode", + "flux2_vae_encode", + "flux_control_lora_loader", + "flux_controlnet", + "flux_denoise", + "flux_denoise_meta", + "flux_fill", + "flux_ip_adapter", + "flux_kontext", + "flux_kontext_image_prep", + "flux_lora_collection_loader", + "flux_lora_loader", + "flux_model_loader", + "flux_redux", + "flux_text_encoder", + "flux_vae_decode", + "flux_vae_encode", + "freeu", + "gemini_image_generation", + "get_image_mask_bounding_box", + "grounding_dino", + "hed_edge_detection", + "heuristic_resize", + "i2l", + "ideal_size", + "if", + "image", + "image_batch", + "image_collection", + "image_generator", + "image_mask_to_tensor", + "image_panel_layout", + "img_blur", + "img_chan", + "img_channel_multiply", + "img_channel_offset", + "img_conv", + "img_crop", + "img_hue_adjust", + "img_hue_adjust_oklch", + "img_ilerp", + "img_lerp", + "img_mul", + "img_noise", + "img_nsfw", + "img_pad_crop", + "img_paste", + "img_resize", + "img_scale", + "img_watermark", + "infill_cv2", + "infill_lama", + "infill_patchmatch", + "infill_rgba", + "infill_tile", + "integer", + "integer_batch", + "integer_collection", + "integer_generator", + "integer_math", + "invert_tensor_mask", + "invokeai_ealightness", + "invokeai_img_blend", + "invokeai_img_composite", + "invokeai_img_dilate_erode", + "invokeai_img_enhance", + "invokeai_img_hue_adjust_plus", + "invokeai_img_val_thresholds", + "ip_adapter", + "iterate", + "l2i", + "latents", + "latents_collection", + "lblend", + "lineart_anime_edge_detection", + "lineart_edge_detection", + "llava_onevision_vllm", + "lora_collection_loader", + "lora_loader", + "lora_selector", + "lresize", + "lscale", + "main_model_loader", + "mask_combine", + "mask_edge", + "mask_from_id", + "mediapipe_face_detection", + "merge_metadata", + "merge_tiles_to_image", + "metadata", + "metadata_field_extractor", + "metadata_from_image", + "metadata_item", + "metadata_item_linked", + "metadata_to_bool", + "metadata_to_bool_collection", + "metadata_to_controlnets", + "metadata_to_float", + "metadata_to_float_collection", + "metadata_to_integer", + "metadata_to_integer_collection", + "metadata_to_ip_adapters", + "metadata_to_lora_collection", + "metadata_to_loras", + "metadata_to_model", + "metadata_to_scheduler", + "metadata_to_sdlx_loras", + "metadata_to_sdxl_model", + "metadata_to_string", + "metadata_to_string_collection", + "metadata_to_t2i_adapters", + "metadata_to_vae", + "mlsd_detection", + "model_identifier", + "mul", + "noise", + "normal_map", + "openai_image_generation", + "pair_tile_image", + "paste_image_into_bounding_box", + "pbr_maps", + "pidi_edge_detection", + "prompt_from_file", + "prompt_template", + "qwen_image_denoise", + "qwen_image_i2l", + "qwen_image_l2i", + "qwen_image_lora_collection_loader", + "qwen_image_lora_loader", + "qwen_image_model_loader", + "qwen_image_text_encoder", + "rand_float", + "rand_int", + "random_range", + "range", + "range_of_size", + "rectangle_mask", + "round_float", + "save_image", + "save_image_to_file", + "scheduler", + "sd3_denoise", + "sd3_i2l", + "sd3_l2i", + "sd3_model_loader", + "sd3_text_encoder", + "sdxl_compel_prompt", + "sdxl_lora_collection_loader", + "sdxl_lora_loader", + "sdxl_model_loader", + "sdxl_refiner_compel_prompt", + "sdxl_refiner_model_loader", + "seamless", + "seedream_image_generation", + "segment_anything", + "show_image", + "spandrel_image_to_image", + "spandrel_image_to_image_autoscale", + "string", + "string_batch", + "string_collection", + "string_generator", + "string_join", + "string_join_three", + "string_replace", + "string_split", + "string_split_neg", + "sub", + "t2i_adapter", + "tensor_mask_to_image", + "text_llm", + "tile_to_properties", + "tiled_multi_diffusion_denoise_latents", + "tomask", + "unsharp_mask", + "unsharp_mask_oklab", + "vae_loader", + "z_image_control", + "z_image_denoise", + "z_image_denoise_meta", + "z_image_i2l", + "z_image_l2i", + "z_image_lora_collection_loader", + "z_image_lora_loader", + "z_image_model_loader", + "z_image_seed_variance_enhancer", + "z_image_text_encoder" + ] + }, + "InvocationProgressEvent": { + "description": "Event model for invocation_progress", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "queue_id": { + "description": "The ID of the queue", + "title": "Queue Id", + "type": "string" + }, + "item_id": { + "description": "The ID of the queue item", + "title": "Item Id", + "type": "integer" + }, + "batch_id": { + "description": "The ID of the queue batch", + "title": "Batch Id", + "type": "string" + }, + "origin": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The origin of the queue item", + "title": "Origin" + }, + "destination": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The destination of the queue item", + "title": "Destination" + }, + "user_id": { + "default": "system", + "description": "The ID of the user who created the queue item", + "title": "User Id", + "type": "string" + }, + "session_id": { + "description": "The ID of the session (aka graph execution state)", + "title": "Session Id", + "type": "string" + }, + "invocation": { + "description": "The ID of the invocation", + "oneOf": [ + { + "$ref": "#/components/schemas/AddInvocation" + }, + { + "$ref": "#/components/schemas/AlibabaCloudImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/AlphaMaskToTensorInvocation" + }, + { + "$ref": "#/components/schemas/AnimaDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/AnimaImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/AnimaLatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/AnimaLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/AnimaLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/AnimaModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/AnimaTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/ApplyMaskTensorToImageInvocation" + }, + { + "$ref": "#/components/schemas/ApplyMaskToImageInvocation" + }, + { + "$ref": "#/components/schemas/BlankImageInvocation" + }, + { + "$ref": "#/components/schemas/BlendLatentsInvocation" + }, + { + "$ref": "#/components/schemas/BooleanCollectionInvocation" + }, + { + "$ref": "#/components/schemas/BooleanInvocation" + }, + { + "$ref": "#/components/schemas/BoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/CLIPSkipInvocation" + }, + { + "$ref": "#/components/schemas/CV2InfillInvocation" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesEvenSplitInvocation" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesInvocation" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesMinimumOverlapInvocation" + }, + { + "$ref": "#/components/schemas/CannyEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/CanvasOutputInvocation" + }, + { + "$ref": "#/components/schemas/CanvasPasteBackInvocation" + }, + { + "$ref": "#/components/schemas/CanvasV2MaskAndCropInvocation" + }, + { + "$ref": "#/components/schemas/CenterPadCropInvocation" + }, + { + "$ref": "#/components/schemas/CogView4DenoiseInvocation" + }, + { + "$ref": "#/components/schemas/CogView4ImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/CogView4LatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/CogView4ModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/CogView4TextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/CollectInvocation" + }, + { + "$ref": "#/components/schemas/ColorCorrectInvocation" + }, + { + "$ref": "#/components/schemas/ColorInvocation" + }, + { + "$ref": "#/components/schemas/ColorMapInvocation" + }, + { + "$ref": "#/components/schemas/CompelInvocation" + }, + { + "$ref": "#/components/schemas/ConditioningCollectionInvocation" + }, + { + "$ref": "#/components/schemas/ConditioningInvocation" + }, + { + "$ref": "#/components/schemas/ContentShuffleInvocation" + }, + { + "$ref": "#/components/schemas/ControlNetInvocation" + }, + { + "$ref": "#/components/schemas/CoreMetadataInvocation" + }, + { + "$ref": "#/components/schemas/CreateDenoiseMaskInvocation" + }, + { + "$ref": "#/components/schemas/CreateGradientMaskInvocation" + }, + { + "$ref": "#/components/schemas/CropImageToBoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/CropLatentsCoreInvocation" + }, + { + "$ref": "#/components/schemas/CvInpaintInvocation" + }, + { + "$ref": "#/components/schemas/DWOpenposeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/DecodeInvisibleWatermarkInvocation" + }, + { + "$ref": "#/components/schemas/DenoiseLatentsInvocation" + }, + { + "$ref": "#/components/schemas/DenoiseLatentsMetaInvocation" + }, + { + "$ref": "#/components/schemas/DepthAnythingDepthEstimationInvocation" + }, + { + "$ref": "#/components/schemas/DivideInvocation" + }, + { + "$ref": "#/components/schemas/DynamicPromptInvocation" + }, + { + "$ref": "#/components/schemas/ESRGANInvocation" + }, + { + "$ref": "#/components/schemas/ExpandMaskWithFadeInvocation" + }, + { + "$ref": "#/components/schemas/FLUXLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/FaceIdentifierInvocation" + }, + { + "$ref": "#/components/schemas/FaceMaskInvocation" + }, + { + "$ref": "#/components/schemas/FaceOffInvocation" + }, + { + "$ref": "#/components/schemas/FloatBatchInvocation" + }, + { + "$ref": "#/components/schemas/FloatCollectionInvocation" + }, + { + "$ref": "#/components/schemas/FloatGenerator" + }, + { + "$ref": "#/components/schemas/FloatInvocation" + }, + { + "$ref": "#/components/schemas/FloatLinearRangeInvocation" + }, + { + "$ref": "#/components/schemas/FloatMathInvocation" + }, + { + "$ref": "#/components/schemas/FloatToIntegerInvocation" + }, + { + "$ref": "#/components/schemas/Flux2DenoiseInvocation" + }, + { + "$ref": "#/components/schemas/Flux2KleinLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/Flux2KleinLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/Flux2KleinModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/Flux2KleinTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/Flux2VaeDecodeInvocation" + }, + { + "$ref": "#/components/schemas/Flux2VaeEncodeInvocation" + }, + { + "$ref": "#/components/schemas/FluxControlLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/FluxControlNetInvocation" + }, + { + "$ref": "#/components/schemas/FluxDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/FluxDenoiseLatentsMetaInvocation" + }, + { + "$ref": "#/components/schemas/FluxFillInvocation" + }, + { + "$ref": "#/components/schemas/FluxIPAdapterInvocation" + }, + { + "$ref": "#/components/schemas/FluxKontextConcatenateImagesInvocation" + }, + { + "$ref": "#/components/schemas/FluxKontextInvocation" + }, + { + "$ref": "#/components/schemas/FluxLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/FluxModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/FluxReduxInvocation" + }, + { + "$ref": "#/components/schemas/FluxTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/FluxVaeDecodeInvocation" + }, + { + "$ref": "#/components/schemas/FluxVaeEncodeInvocation" + }, + { + "$ref": "#/components/schemas/FreeUInvocation" + }, + { + "$ref": "#/components/schemas/GeminiImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/GetMaskBoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/GroundingDinoInvocation" + }, + { + "$ref": "#/components/schemas/HEDEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/HeuristicResizeInvocation" + }, + { + "$ref": "#/components/schemas/IPAdapterInvocation" + }, + { + "$ref": "#/components/schemas/IdealSizeInvocation" + }, + { + "$ref": "#/components/schemas/IfInvocation" + }, + { + "$ref": "#/components/schemas/ImageBatchInvocation" + }, + { + "$ref": "#/components/schemas/ImageBlurInvocation" + }, + { + "$ref": "#/components/schemas/ImageChannelInvocation" + }, + { + "$ref": "#/components/schemas/ImageChannelMultiplyInvocation" + }, + { + "$ref": "#/components/schemas/ImageChannelOffsetInvocation" + }, + { + "$ref": "#/components/schemas/ImageCollectionInvocation" + }, + { + "$ref": "#/components/schemas/ImageConvertInvocation" + }, + { + "$ref": "#/components/schemas/ImageCropInvocation" + }, + { + "$ref": "#/components/schemas/ImageGenerator" + }, + { + "$ref": "#/components/schemas/ImageHueAdjustmentInvocation" + }, + { + "$ref": "#/components/schemas/ImageInverseLerpInvocation" + }, + { + "$ref": "#/components/schemas/ImageInvocation" + }, + { + "$ref": "#/components/schemas/ImageLerpInvocation" + }, + { + "$ref": "#/components/schemas/ImageMaskToTensorInvocation" + }, + { + "$ref": "#/components/schemas/ImageMultiplyInvocation" + }, + { + "$ref": "#/components/schemas/ImageNSFWBlurInvocation" + }, + { + "$ref": "#/components/schemas/ImageNoiseInvocation" + }, + { + "$ref": "#/components/schemas/ImagePanelLayoutInvocation" + }, + { + "$ref": "#/components/schemas/ImagePasteInvocation" + }, + { + "$ref": "#/components/schemas/ImageResizeInvocation" + }, + { + "$ref": "#/components/schemas/ImageScaleInvocation" + }, + { + "$ref": "#/components/schemas/ImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/ImageWatermarkInvocation" + }, + { + "$ref": "#/components/schemas/InfillColorInvocation" + }, + { + "$ref": "#/components/schemas/InfillPatchMatchInvocation" + }, + { + "$ref": "#/components/schemas/InfillTileInvocation" + }, + { + "$ref": "#/components/schemas/IntegerBatchInvocation" + }, + { + "$ref": "#/components/schemas/IntegerCollectionInvocation" + }, + { + "$ref": "#/components/schemas/IntegerGenerator" + }, + { + "$ref": "#/components/schemas/IntegerInvocation" + }, + { + "$ref": "#/components/schemas/IntegerMathInvocation" + }, + { + "$ref": "#/components/schemas/InvertTensorMaskInvocation" + }, + { + "$ref": "#/components/schemas/InvokeAdjustImageHuePlusInvocation" + }, + { + "$ref": "#/components/schemas/InvokeEquivalentAchromaticLightnessInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageBlendInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageCompositorInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageDilateOrErodeInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageEnhanceInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageValueThresholdsInvocation" + }, + { + "$ref": "#/components/schemas/IterateInvocation" + }, + { + "$ref": "#/components/schemas/LaMaInfillInvocation" + }, + { + "$ref": "#/components/schemas/LatentsCollectionInvocation" + }, + { + "$ref": "#/components/schemas/LatentsInvocation" + }, + { + "$ref": "#/components/schemas/LatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/LineartAnimeEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/LineartEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/LlavaOnevisionVllmInvocation" + }, + { + "$ref": "#/components/schemas/LoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/LoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/LoRASelectorInvocation" + }, + { + "$ref": "#/components/schemas/MLSDDetectionInvocation" + }, + { + "$ref": "#/components/schemas/MainModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/MaskCombineInvocation" + }, + { + "$ref": "#/components/schemas/MaskEdgeInvocation" + }, + { + "$ref": "#/components/schemas/MaskFromAlphaInvocation" + }, + { + "$ref": "#/components/schemas/MaskFromIDInvocation" + }, + { + "$ref": "#/components/schemas/MaskTensorToImageInvocation" + }, + { + "$ref": "#/components/schemas/MediaPipeFaceDetectionInvocation" + }, + { + "$ref": "#/components/schemas/MergeMetadataInvocation" + }, + { + "$ref": "#/components/schemas/MergeTilesToImageInvocation" + }, + { + "$ref": "#/components/schemas/MetadataFieldExtractorInvocation" + }, + { + "$ref": "#/components/schemas/MetadataFromImageInvocation" + }, + { + "$ref": "#/components/schemas/MetadataInvocation" + }, + { + "$ref": "#/components/schemas/MetadataItemInvocation" + }, + { + "$ref": "#/components/schemas/MetadataItemLinkedInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToBoolCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToBoolInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToControlnetsInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToFloatCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToFloatInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToIPAdaptersInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToIntegerCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToIntegerInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToLorasCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToLorasInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToModelInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToSDXLLorasInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToSDXLModelInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToSchedulerInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToStringCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToStringInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToT2IAdaptersInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToVAEInvocation" + }, + { + "$ref": "#/components/schemas/ModelIdentifierInvocation" + }, + { + "$ref": "#/components/schemas/MultiplyInvocation" + }, + { + "$ref": "#/components/schemas/NoiseInvocation" + }, + { + "$ref": "#/components/schemas/NormalMapInvocation" + }, + { + "$ref": "#/components/schemas/OklabUnsharpMaskInvocation" + }, + { + "$ref": "#/components/schemas/OklchImageHueAdjustmentInvocation" + }, + { + "$ref": "#/components/schemas/OpenAIImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/PBRMapsInvocation" + }, + { + "$ref": "#/components/schemas/PairTileImageInvocation" + }, + { + "$ref": "#/components/schemas/PasteImageIntoBoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/PiDiNetEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/PromptTemplateInvocation" + }, + { + "$ref": "#/components/schemas/PromptsFromFileInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageLatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/QwenImageLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/RandomFloatInvocation" + }, + { + "$ref": "#/components/schemas/RandomIntInvocation" + }, + { + "$ref": "#/components/schemas/RandomRangeInvocation" + }, + { + "$ref": "#/components/schemas/RangeInvocation" + }, + { + "$ref": "#/components/schemas/RangeOfSizeInvocation" + }, + { + "$ref": "#/components/schemas/RectangleMaskInvocation" + }, + { + "$ref": "#/components/schemas/ResizeLatentsInvocation" + }, + { + "$ref": "#/components/schemas/RoundInvocation" + }, + { + "$ref": "#/components/schemas/SD3DenoiseInvocation" + }, + { + "$ref": "#/components/schemas/SD3ImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/SD3LatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/SDXLCompelPromptInvocation" + }, + { + "$ref": "#/components/schemas/SDXLLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/SDXLLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/SDXLModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/SDXLRefinerCompelPromptInvocation" + }, + { + "$ref": "#/components/schemas/SDXLRefinerModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/SaveImageInvocation" + }, + { + "$ref": "#/components/schemas/SaveImageToFileInvocation" + }, + { + "$ref": "#/components/schemas/ScaleLatentsInvocation" + }, + { + "$ref": "#/components/schemas/SchedulerInvocation" + }, + { + "$ref": "#/components/schemas/Sd3ModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/Sd3TextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/SeamlessModeInvocation" + }, + { + "$ref": "#/components/schemas/SeedreamImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/SegmentAnythingInvocation" + }, + { + "$ref": "#/components/schemas/ShowImageInvocation" + }, + { + "$ref": "#/components/schemas/SpandrelImageToImageAutoscaleInvocation" + }, + { + "$ref": "#/components/schemas/SpandrelImageToImageInvocation" + }, + { + "$ref": "#/components/schemas/StringBatchInvocation" + }, + { + "$ref": "#/components/schemas/StringCollectionInvocation" + }, + { + "$ref": "#/components/schemas/StringGenerator" + }, + { + "$ref": "#/components/schemas/StringInvocation" + }, + { + "$ref": "#/components/schemas/StringJoinInvocation" + }, + { + "$ref": "#/components/schemas/StringJoinThreeInvocation" + }, + { + "$ref": "#/components/schemas/StringReplaceInvocation" + }, + { + "$ref": "#/components/schemas/StringSplitInvocation" + }, + { + "$ref": "#/components/schemas/StringSplitNegInvocation" + }, + { + "$ref": "#/components/schemas/SubtractInvocation" + }, + { + "$ref": "#/components/schemas/T2IAdapterInvocation" + }, + { + "$ref": "#/components/schemas/TextLLMInvocation" + }, + { + "$ref": "#/components/schemas/TileToPropertiesInvocation" + }, + { + "$ref": "#/components/schemas/TiledMultiDiffusionDenoiseLatents" + }, + { + "$ref": "#/components/schemas/UnsharpMaskInvocation" + }, + { + "$ref": "#/components/schemas/VAELoaderInvocation" + }, + { + "$ref": "#/components/schemas/ZImageControlInvocation" + }, + { + "$ref": "#/components/schemas/ZImageDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/ZImageDenoiseMetaInvocation" + }, + { + "$ref": "#/components/schemas/ZImageImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/ZImageLatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/ZImageLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/ZImageLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/ZImageModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/ZImageSeedVarianceEnhancerInvocation" + }, + { + "$ref": "#/components/schemas/ZImageTextEncoderInvocation" + } + ], + "title": "Invocation" + }, + "invocation_source_id": { + "description": "The ID of the prepared invocation's source node", + "title": "Invocation Source Id", + "type": "string" + }, + "message": { + "description": "A message to display", + "title": "Message", + "type": "string" + }, + "percentage": { + "anyOf": [ + { + "maximum": 1, + "minimum": 0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The percentage of the progress (omit to indicate indeterminate progress)", + "title": "Percentage" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProgressImage" + }, + { + "type": "null" + } + ], + "default": null, + "description": "An image representing the current state of the progress" + } + }, + "required": [ + "timestamp", + "queue_id", + "item_id", + "batch_id", + "origin", + "destination", + "user_id", + "session_id", + "invocation", + "invocation_source_id", + "message", + "percentage", + "image" + ], + "title": "InvocationProgressEvent", + "type": "object" + }, + "InvocationStartedEvent": { + "description": "Event model for invocation_started", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "queue_id": { + "description": "The ID of the queue", + "title": "Queue Id", + "type": "string" + }, + "item_id": { + "description": "The ID of the queue item", + "title": "Item Id", + "type": "integer" + }, + "batch_id": { + "description": "The ID of the queue batch", + "title": "Batch Id", + "type": "string" + }, + "origin": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The origin of the queue item", + "title": "Origin" + }, + "destination": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The destination of the queue item", + "title": "Destination" + }, + "user_id": { + "default": "system", + "description": "The ID of the user who created the queue item", + "title": "User Id", + "type": "string" + }, + "session_id": { + "description": "The ID of the session (aka graph execution state)", + "title": "Session Id", + "type": "string" + }, + "invocation": { + "description": "The ID of the invocation", + "oneOf": [ + { + "$ref": "#/components/schemas/AddInvocation" + }, + { + "$ref": "#/components/schemas/AlibabaCloudImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/AlphaMaskToTensorInvocation" + }, + { + "$ref": "#/components/schemas/AnimaDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/AnimaImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/AnimaLatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/AnimaLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/AnimaLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/AnimaModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/AnimaTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/ApplyMaskTensorToImageInvocation" + }, + { + "$ref": "#/components/schemas/ApplyMaskToImageInvocation" + }, + { + "$ref": "#/components/schemas/BlankImageInvocation" + }, + { + "$ref": "#/components/schemas/BlendLatentsInvocation" + }, + { + "$ref": "#/components/schemas/BooleanCollectionInvocation" + }, + { + "$ref": "#/components/schemas/BooleanInvocation" + }, + { + "$ref": "#/components/schemas/BoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/CLIPSkipInvocation" + }, + { + "$ref": "#/components/schemas/CV2InfillInvocation" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesEvenSplitInvocation" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesInvocation" + }, + { + "$ref": "#/components/schemas/CalculateImageTilesMinimumOverlapInvocation" + }, + { + "$ref": "#/components/schemas/CannyEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/CanvasOutputInvocation" + }, + { + "$ref": "#/components/schemas/CanvasPasteBackInvocation" + }, + { + "$ref": "#/components/schemas/CanvasV2MaskAndCropInvocation" + }, + { + "$ref": "#/components/schemas/CenterPadCropInvocation" + }, + { + "$ref": "#/components/schemas/CogView4DenoiseInvocation" + }, + { + "$ref": "#/components/schemas/CogView4ImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/CogView4LatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/CogView4ModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/CogView4TextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/CollectInvocation" + }, + { + "$ref": "#/components/schemas/ColorCorrectInvocation" + }, + { + "$ref": "#/components/schemas/ColorInvocation" + }, + { + "$ref": "#/components/schemas/ColorMapInvocation" + }, + { + "$ref": "#/components/schemas/CompelInvocation" + }, + { + "$ref": "#/components/schemas/ConditioningCollectionInvocation" + }, + { + "$ref": "#/components/schemas/ConditioningInvocation" + }, + { + "$ref": "#/components/schemas/ContentShuffleInvocation" + }, + { + "$ref": "#/components/schemas/ControlNetInvocation" + }, + { + "$ref": "#/components/schemas/CoreMetadataInvocation" + }, + { + "$ref": "#/components/schemas/CreateDenoiseMaskInvocation" + }, + { + "$ref": "#/components/schemas/CreateGradientMaskInvocation" + }, + { + "$ref": "#/components/schemas/CropImageToBoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/CropLatentsCoreInvocation" + }, + { + "$ref": "#/components/schemas/CvInpaintInvocation" + }, + { + "$ref": "#/components/schemas/DWOpenposeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/DecodeInvisibleWatermarkInvocation" + }, + { + "$ref": "#/components/schemas/DenoiseLatentsInvocation" + }, + { + "$ref": "#/components/schemas/DenoiseLatentsMetaInvocation" + }, + { + "$ref": "#/components/schemas/DepthAnythingDepthEstimationInvocation" + }, + { + "$ref": "#/components/schemas/DivideInvocation" + }, + { + "$ref": "#/components/schemas/DynamicPromptInvocation" + }, + { + "$ref": "#/components/schemas/ESRGANInvocation" + }, + { + "$ref": "#/components/schemas/ExpandMaskWithFadeInvocation" + }, + { + "$ref": "#/components/schemas/FLUXLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/FaceIdentifierInvocation" + }, + { + "$ref": "#/components/schemas/FaceMaskInvocation" + }, + { + "$ref": "#/components/schemas/FaceOffInvocation" + }, + { + "$ref": "#/components/schemas/FloatBatchInvocation" + }, + { + "$ref": "#/components/schemas/FloatCollectionInvocation" + }, + { + "$ref": "#/components/schemas/FloatGenerator" + }, + { + "$ref": "#/components/schemas/FloatInvocation" + }, + { + "$ref": "#/components/schemas/FloatLinearRangeInvocation" + }, + { + "$ref": "#/components/schemas/FloatMathInvocation" + }, + { + "$ref": "#/components/schemas/FloatToIntegerInvocation" + }, + { + "$ref": "#/components/schemas/Flux2DenoiseInvocation" + }, + { + "$ref": "#/components/schemas/Flux2KleinLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/Flux2KleinLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/Flux2KleinModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/Flux2KleinTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/Flux2VaeDecodeInvocation" + }, + { + "$ref": "#/components/schemas/Flux2VaeEncodeInvocation" + }, + { + "$ref": "#/components/schemas/FluxControlLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/FluxControlNetInvocation" + }, + { + "$ref": "#/components/schemas/FluxDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/FluxDenoiseLatentsMetaInvocation" + }, + { + "$ref": "#/components/schemas/FluxFillInvocation" + }, + { + "$ref": "#/components/schemas/FluxIPAdapterInvocation" + }, + { + "$ref": "#/components/schemas/FluxKontextConcatenateImagesInvocation" + }, + { + "$ref": "#/components/schemas/FluxKontextInvocation" + }, + { + "$ref": "#/components/schemas/FluxLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/FluxModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/FluxReduxInvocation" + }, + { + "$ref": "#/components/schemas/FluxTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/FluxVaeDecodeInvocation" + }, + { + "$ref": "#/components/schemas/FluxVaeEncodeInvocation" + }, + { + "$ref": "#/components/schemas/FreeUInvocation" + }, + { + "$ref": "#/components/schemas/GeminiImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/GetMaskBoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/GroundingDinoInvocation" + }, + { + "$ref": "#/components/schemas/HEDEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/HeuristicResizeInvocation" + }, + { + "$ref": "#/components/schemas/IPAdapterInvocation" + }, + { + "$ref": "#/components/schemas/IdealSizeInvocation" + }, + { + "$ref": "#/components/schemas/IfInvocation" + }, + { + "$ref": "#/components/schemas/ImageBatchInvocation" + }, + { + "$ref": "#/components/schemas/ImageBlurInvocation" + }, + { + "$ref": "#/components/schemas/ImageChannelInvocation" + }, + { + "$ref": "#/components/schemas/ImageChannelMultiplyInvocation" + }, + { + "$ref": "#/components/schemas/ImageChannelOffsetInvocation" + }, + { + "$ref": "#/components/schemas/ImageCollectionInvocation" + }, + { + "$ref": "#/components/schemas/ImageConvertInvocation" + }, + { + "$ref": "#/components/schemas/ImageCropInvocation" + }, + { + "$ref": "#/components/schemas/ImageGenerator" + }, + { + "$ref": "#/components/schemas/ImageHueAdjustmentInvocation" + }, + { + "$ref": "#/components/schemas/ImageInverseLerpInvocation" + }, + { + "$ref": "#/components/schemas/ImageInvocation" + }, + { + "$ref": "#/components/schemas/ImageLerpInvocation" + }, + { + "$ref": "#/components/schemas/ImageMaskToTensorInvocation" + }, + { + "$ref": "#/components/schemas/ImageMultiplyInvocation" + }, + { + "$ref": "#/components/schemas/ImageNSFWBlurInvocation" + }, + { + "$ref": "#/components/schemas/ImageNoiseInvocation" + }, + { + "$ref": "#/components/schemas/ImagePanelLayoutInvocation" + }, + { + "$ref": "#/components/schemas/ImagePasteInvocation" + }, + { + "$ref": "#/components/schemas/ImageResizeInvocation" + }, + { + "$ref": "#/components/schemas/ImageScaleInvocation" + }, + { + "$ref": "#/components/schemas/ImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/ImageWatermarkInvocation" + }, + { + "$ref": "#/components/schemas/InfillColorInvocation" + }, + { + "$ref": "#/components/schemas/InfillPatchMatchInvocation" + }, + { + "$ref": "#/components/schemas/InfillTileInvocation" + }, + { + "$ref": "#/components/schemas/IntegerBatchInvocation" + }, + { + "$ref": "#/components/schemas/IntegerCollectionInvocation" + }, + { + "$ref": "#/components/schemas/IntegerGenerator" + }, + { + "$ref": "#/components/schemas/IntegerInvocation" + }, + { + "$ref": "#/components/schemas/IntegerMathInvocation" + }, + { + "$ref": "#/components/schemas/InvertTensorMaskInvocation" + }, + { + "$ref": "#/components/schemas/InvokeAdjustImageHuePlusInvocation" + }, + { + "$ref": "#/components/schemas/InvokeEquivalentAchromaticLightnessInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageBlendInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageCompositorInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageDilateOrErodeInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageEnhanceInvocation" + }, + { + "$ref": "#/components/schemas/InvokeImageValueThresholdsInvocation" + }, + { + "$ref": "#/components/schemas/IterateInvocation" + }, + { + "$ref": "#/components/schemas/LaMaInfillInvocation" + }, + { + "$ref": "#/components/schemas/LatentsCollectionInvocation" + }, + { + "$ref": "#/components/schemas/LatentsInvocation" + }, + { + "$ref": "#/components/schemas/LatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/LineartAnimeEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/LineartEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/LlavaOnevisionVllmInvocation" + }, + { + "$ref": "#/components/schemas/LoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/LoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/LoRASelectorInvocation" + }, + { + "$ref": "#/components/schemas/MLSDDetectionInvocation" + }, + { + "$ref": "#/components/schemas/MainModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/MaskCombineInvocation" + }, + { + "$ref": "#/components/schemas/MaskEdgeInvocation" + }, + { + "$ref": "#/components/schemas/MaskFromAlphaInvocation" + }, + { + "$ref": "#/components/schemas/MaskFromIDInvocation" + }, + { + "$ref": "#/components/schemas/MaskTensorToImageInvocation" + }, + { + "$ref": "#/components/schemas/MediaPipeFaceDetectionInvocation" + }, + { + "$ref": "#/components/schemas/MergeMetadataInvocation" + }, + { + "$ref": "#/components/schemas/MergeTilesToImageInvocation" + }, + { + "$ref": "#/components/schemas/MetadataFieldExtractorInvocation" + }, + { + "$ref": "#/components/schemas/MetadataFromImageInvocation" + }, + { + "$ref": "#/components/schemas/MetadataInvocation" + }, + { + "$ref": "#/components/schemas/MetadataItemInvocation" + }, + { + "$ref": "#/components/schemas/MetadataItemLinkedInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToBoolCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToBoolInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToControlnetsInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToFloatCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToFloatInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToIPAdaptersInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToIntegerCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToIntegerInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToLorasCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToLorasInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToModelInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToSDXLLorasInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToSDXLModelInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToSchedulerInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToStringCollectionInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToStringInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToT2IAdaptersInvocation" + }, + { + "$ref": "#/components/schemas/MetadataToVAEInvocation" + }, + { + "$ref": "#/components/schemas/ModelIdentifierInvocation" + }, + { + "$ref": "#/components/schemas/MultiplyInvocation" + }, + { + "$ref": "#/components/schemas/NoiseInvocation" + }, + { + "$ref": "#/components/schemas/NormalMapInvocation" + }, + { + "$ref": "#/components/schemas/OklabUnsharpMaskInvocation" + }, + { + "$ref": "#/components/schemas/OklchImageHueAdjustmentInvocation" + }, + { + "$ref": "#/components/schemas/OpenAIImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/PBRMapsInvocation" + }, + { + "$ref": "#/components/schemas/PairTileImageInvocation" + }, + { + "$ref": "#/components/schemas/PasteImageIntoBoundingBoxInvocation" + }, + { + "$ref": "#/components/schemas/PiDiNetEdgeDetectionInvocation" + }, + { + "$ref": "#/components/schemas/PromptTemplateInvocation" + }, + { + "$ref": "#/components/schemas/PromptsFromFileInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageLatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/QwenImageLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/QwenImageTextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/RandomFloatInvocation" + }, + { + "$ref": "#/components/schemas/RandomIntInvocation" + }, + { + "$ref": "#/components/schemas/RandomRangeInvocation" + }, + { + "$ref": "#/components/schemas/RangeInvocation" + }, + { + "$ref": "#/components/schemas/RangeOfSizeInvocation" + }, + { + "$ref": "#/components/schemas/RectangleMaskInvocation" + }, + { + "$ref": "#/components/schemas/ResizeLatentsInvocation" + }, + { + "$ref": "#/components/schemas/RoundInvocation" + }, + { + "$ref": "#/components/schemas/SD3DenoiseInvocation" + }, + { + "$ref": "#/components/schemas/SD3ImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/SD3LatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/SDXLCompelPromptInvocation" + }, + { + "$ref": "#/components/schemas/SDXLLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/SDXLLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/SDXLModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/SDXLRefinerCompelPromptInvocation" + }, + { + "$ref": "#/components/schemas/SDXLRefinerModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/SaveImageInvocation" + }, + { + "$ref": "#/components/schemas/SaveImageToFileInvocation" + }, + { + "$ref": "#/components/schemas/ScaleLatentsInvocation" + }, + { + "$ref": "#/components/schemas/SchedulerInvocation" + }, + { + "$ref": "#/components/schemas/Sd3ModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/Sd3TextEncoderInvocation" + }, + { + "$ref": "#/components/schemas/SeamlessModeInvocation" + }, + { + "$ref": "#/components/schemas/SeedreamImageGenerationInvocation" + }, + { + "$ref": "#/components/schemas/SegmentAnythingInvocation" + }, + { + "$ref": "#/components/schemas/ShowImageInvocation" + }, + { + "$ref": "#/components/schemas/SpandrelImageToImageAutoscaleInvocation" + }, + { + "$ref": "#/components/schemas/SpandrelImageToImageInvocation" + }, + { + "$ref": "#/components/schemas/StringBatchInvocation" + }, + { + "$ref": "#/components/schemas/StringCollectionInvocation" + }, + { + "$ref": "#/components/schemas/StringGenerator" + }, + { + "$ref": "#/components/schemas/StringInvocation" + }, + { + "$ref": "#/components/schemas/StringJoinInvocation" + }, + { + "$ref": "#/components/schemas/StringJoinThreeInvocation" + }, + { + "$ref": "#/components/schemas/StringReplaceInvocation" + }, + { + "$ref": "#/components/schemas/StringSplitInvocation" + }, + { + "$ref": "#/components/schemas/StringSplitNegInvocation" + }, + { + "$ref": "#/components/schemas/SubtractInvocation" + }, + { + "$ref": "#/components/schemas/T2IAdapterInvocation" + }, + { + "$ref": "#/components/schemas/TextLLMInvocation" + }, + { + "$ref": "#/components/schemas/TileToPropertiesInvocation" + }, + { + "$ref": "#/components/schemas/TiledMultiDiffusionDenoiseLatents" + }, + { + "$ref": "#/components/schemas/UnsharpMaskInvocation" + }, + { + "$ref": "#/components/schemas/VAELoaderInvocation" + }, + { + "$ref": "#/components/schemas/ZImageControlInvocation" + }, + { + "$ref": "#/components/schemas/ZImageDenoiseInvocation" + }, + { + "$ref": "#/components/schemas/ZImageDenoiseMetaInvocation" + }, + { + "$ref": "#/components/schemas/ZImageImageToLatentsInvocation" + }, + { + "$ref": "#/components/schemas/ZImageLatentsToImageInvocation" + }, + { + "$ref": "#/components/schemas/ZImageLoRACollectionLoader" + }, + { + "$ref": "#/components/schemas/ZImageLoRALoaderInvocation" + }, + { + "$ref": "#/components/schemas/ZImageModelLoaderInvocation" + }, + { + "$ref": "#/components/schemas/ZImageSeedVarianceEnhancerInvocation" + }, + { + "$ref": "#/components/schemas/ZImageTextEncoderInvocation" + } + ], + "title": "Invocation" + }, + "invocation_source_id": { + "description": "The ID of the prepared invocation's source node", + "title": "Invocation Source Id", + "type": "string" + } + }, + "required": [ + "timestamp", + "queue_id", + "item_id", + "batch_id", + "origin", + "destination", + "user_id", + "session_id", + "invocation", + "invocation_source_id" + ], + "title": "InvocationStartedEvent", + "type": "object" + }, + "InvokeAIAppConfig": { + "properties": { + "schema_version": { + "type": "string", + "title": "Schema Version", + "description": "Schema version of the config file. This is not a user-configurable setting.", + "default": "4.0.3" + }, + "legacy_models_yaml_path": { + "anyOf": [ + { + "type": "string", + "format": "path" + }, + { + "type": "null" + } + ], + "title": "Legacy Models Yaml Path", + "description": "Path to the legacy models.yaml file. This is not a user-configurable setting." + }, + "host": { + "type": "string", + "title": "Host", + "description": "IP address to bind to. Use `0.0.0.0` to serve to your local network.", + "default": "127.0.0.1" + }, + "port": { + "type": "integer", + "title": "Port", + "description": "Port to bind to.", + "default": 9090 + }, + "allow_origins": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Allow Origins", + "description": "Allowed CORS origins.", + "default": [] + }, + "allow_credentials": { + "type": "boolean", + "title": "Allow Credentials", + "description": "Allow CORS credentials.", + "default": true + }, + "allow_methods": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Allow Methods", + "description": "Methods allowed for CORS.", + "default": ["*"] + }, + "allow_headers": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Allow Headers", + "description": "Headers allowed for CORS.", + "default": ["*"] + }, + "ssl_certfile": { + "anyOf": [ + { + "type": "string", + "format": "path" + }, + { + "type": "null" + } + ], + "title": "Ssl Certfile", + "description": "SSL certificate file for HTTPS. See https://www.uvicorn.dev/settings/#https." + }, + "ssl_keyfile": { + "anyOf": [ + { + "type": "string", + "format": "path" + }, + { + "type": "null" + } + ], + "title": "Ssl Keyfile", + "description": "SSL key file for HTTPS. See https://www.uvicorn.dev/settings/#https." + }, + "log_tokenization": { + "type": "boolean", + "title": "Log Tokenization", + "description": "Enable logging of parsed prompt tokens.", + "default": false + }, + "patchmatch": { + "type": "boolean", + "title": "Patchmatch", + "description": "Enable patchmatch inpaint code.", + "default": true + }, + "models_dir": { + "type": "string", + "format": "path", + "title": "Models Dir", + "description": "Path to the models directory.", + "default": "models" + }, + "convert_cache_dir": { + "type": "string", + "format": "path", + "title": "Convert Cache Dir", + "description": "Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions).", + "default": "models/.convert_cache" + }, + "download_cache_dir": { + "type": "string", + "format": "path", + "title": "Download Cache Dir", + "description": "Path to the directory that contains dynamically downloaded models.", + "default": "models/.download_cache" + }, + "legacy_conf_dir": { + "type": "string", + "format": "path", + "title": "Legacy Conf Dir", + "description": "Path to directory of legacy checkpoint config files.", + "default": "configs" + }, + "db_dir": { + "type": "string", + "format": "path", + "title": "Db Dir", + "description": "Path to InvokeAI databases directory.", + "default": "databases" + }, + "outputs_dir": { + "type": "string", + "format": "path", + "title": "Outputs Dir", + "description": "Path to directory for outputs.", + "default": "outputs" + }, + "image_subfolder_strategy": { + "type": "string", + "enum": ["flat", "date", "type", "hash"], + "title": "Image Subfolder Strategy", + "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.", + "default": "flat" + }, + "custom_nodes_dir": { + "type": "string", + "format": "path", + "title": "Custom Nodes Dir", + "description": "Path to directory for custom nodes.", + "default": "nodes" + }, + "style_presets_dir": { + "type": "string", + "format": "path", + "title": "Style Presets Dir", + "description": "Path to directory for style presets.", + "default": "style_presets" + }, + "workflow_thumbnails_dir": { + "type": "string", + "format": "path", + "title": "Workflow Thumbnails Dir", + "description": "Path to directory for workflow thumbnails.", + "default": "workflow_thumbnails" + }, + "log_handlers": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Log Handlers", + "description": "Log handler. Valid options are \"console\", \"file=\", \"syslog=path|address:host:port\", \"http=\".", + "default": ["console"] + }, + "log_format": { + "type": "string", + "enum": ["plain", "color", "syslog", "legacy"], + "title": "Log Format", + "description": "Log format. Use \"plain\" for text-only, \"color\" for colorized output, \"legacy\" for 2.3-style logging and \"syslog\" for syslog-style.", + "default": "color" + }, + "log_level": { + "type": "string", + "enum": ["debug", "info", "warning", "error", "critical"], + "title": "Log Level", + "description": "Emit logging messages at this level or higher.", + "default": "info" + }, + "log_sql": { + "type": "boolean", + "title": "Log Sql", + "description": "Log SQL queries. `log_level` must be `debug` for this to do anything. Extremely verbose.", + "default": false + }, + "log_level_network": { + "type": "string", + "enum": ["debug", "info", "warning", "error", "critical"], + "title": "Log Level Network", + "description": "Log level for network-related messages. 'info' and 'debug' are very verbose.", + "default": "warning" + }, + "use_memory_db": { + "type": "boolean", + "title": "Use Memory Db", + "description": "Use in-memory database. Useful for development.", + "default": false + }, + "dev_reload": { + "type": "boolean", + "title": "Dev Reload", + "description": "Automatically reload when Python sources are changed. Does not reload node definitions.", + "default": false + }, + "profile_graphs": { + "type": "boolean", + "title": "Profile Graphs", + "description": "Enable graph profiling using `cProfile`.", + "default": false + }, + "profile_prefix": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Profile Prefix", + "description": "An optional prefix for profile output files." + }, + "profiles_dir": { + "type": "string", + "format": "path", + "title": "Profiles Dir", + "description": "Path to profiles output directory.", + "default": "profiles" + }, + "max_cache_ram_gb": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Max Cache Ram Gb", + "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": { + "anyOf": [ + { + "type": "number", + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Max Cache Vram Gb", + "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": { + "type": "boolean", + "title": "Log Memory Usage", + "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.", + "default": false + }, + "model_cache_keep_alive_min": { + "type": "number", + "minimum": 0.0, + "title": "Model Cache Keep Alive Min", + "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.", + "default": 0 + }, + "device_working_mem_gb": { + "type": "number", + "title": "Device Working Mem Gb", + "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.", + "default": 3 + }, + "enable_partial_loading": { + "type": "boolean", + "title": "Enable Partial Loading", + "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.", + "default": true + }, + "keep_ram_copy_of_weights": { + "type": "boolean", + "title": "Keep Ram Copy Of Weights", + "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.", + "default": true + }, + "ram": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Ram", + "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": { + "anyOf": [ + { + "type": "number", + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Vram", + "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": { + "type": "boolean", + "title": "Lazy Offload", + "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.", + "default": true + }, + "pytorch_cuda_alloc_conf": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Pytorch Cuda Alloc Conf", + "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": { + "type": "string", + "pattern": "^(auto|cpu|mps|cuda(:\\d+)?)$", + "title": "Device", + "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)", + "default": "auto" + }, + "precision": { + "type": "string", + "enum": ["auto", "float16", "bfloat16", "float32"], + "title": "Precision", + "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.", + "default": "auto" + }, + "sequential_guidance": { + "type": "boolean", + "title": "Sequential Guidance", + "description": "Whether to calculate guidance in serial instead of in parallel, lowering memory requirements.", + "default": false + }, + "attention_type": { + "type": "string", + "enum": ["auto", "normal", "xformers", "sliced", "torch-sdp"], + "title": "Attention Type", + "description": "Attention type.", + "default": "auto" + }, + "attention_slice_size": { + "enum": ["auto", "balanced", "max", 1, 2, 3, 4, 5, 6, 7, 8], + "title": "Attention Slice Size", + "description": "Slice size, valid when attention_type==\"sliced\".", + "default": "auto" + }, + "force_tiled_decode": { + "type": "boolean", + "title": "Force Tiled Decode", + "description": "Whether to enable tiled VAE decode (reduces memory consumption with some performance penalty).", + "default": false + }, + "pil_compress_level": { + "type": "integer", + "title": "Pil Compress Level", + "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.", + "default": 1 + }, + "max_queue_size": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Max Queue Size", + "description": "Maximum number of items in the session queue.", + "default": 10000 + }, + "clear_queue_on_startup": { + "type": "boolean", + "title": "Clear Queue On Startup", + "description": "Empties session queue on startup. If true, disables `max_queue_history`.", + "default": false + }, + "max_queue_history": { + "anyOf": [ + { + "type": "integer", + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Max Queue History", + "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." + }, + "allow_nodes": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Allow Nodes", + "description": "List of nodes to allow. Omit to allow all." + }, + "deny_nodes": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Deny Nodes", + "description": "List of nodes to deny. Omit to deny none." + }, + "node_cache_size": { + "type": "integer", + "title": "Node Cache Size", + "description": "How many cached nodes to keep in memory.", + "default": 512 + }, + "hashing_algorithm": { + "type": "string", + "enum": [ + "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" + ], + "title": "Hashing Algorithm", + "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.", + "default": "blake3_single" + }, + "remote_api_tokens": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/URLRegexTokenPair" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Remote Api Tokens", + "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": { + "type": "boolean", + "title": "Scan Models On Startup", + "description": "Scan the models directory on startup, registering orphaned models. This is typically only used in conjunction with `use_memory_db` for testing purposes.", + "default": false + }, + "unsafe_disable_picklescan": { + "type": "boolean", + "title": "Unsafe Disable Picklescan", + "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.", + "default": false + }, + "allow_unknown_models": { + "type": "boolean", + "title": "Allow Unknown Models", + "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.", + "default": true + }, + "multiuser": { + "type": "boolean", + "title": "Multiuser", + "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.", + "default": false + }, + "strict_password_checking": { + "type": "boolean", + "title": "Strict Password Checking", + "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.", + "default": false + }, + "external_alibabacloud_api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Alibabacloud Api Key", + "description": "API key for Alibaba Cloud DashScope image generation." + }, + "external_alibabacloud_base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Alibabacloud Base Url", + "description": "Base URL override for Alibaba Cloud DashScope image generation." + }, + "external_gemini_api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Gemini Api Key", + "description": "API key for Gemini image generation." + }, + "external_openai_api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Openai Api Key", + "description": "API key for OpenAI image generation." + }, + "external_gemini_base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Gemini Base Url", + "description": "Base URL override for Gemini image generation." + }, + "external_openai_base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Openai Base Url", + "description": "Base URL override for OpenAI image generation." + }, + "external_seedream_api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Seedream Api Key", + "description": "API key for Seedream image generation." + }, + "external_seedream_base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Seedream Base Url", + "description": "Base URL override for Seedream image generation." + } + }, + "additionalProperties": false, + "type": "object", + "title": "InvokeAIAppConfig", + "description": "Invoke's global app configuration.\n\nTypically, 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.\n\nAttributes:\n host: IP address to bind to. Use `0.0.0.0` to serve to your local network.\n port: Port to bind to.\n allow_origins: Allowed CORS origins.\n allow_credentials: Allow CORS credentials.\n allow_methods: Methods allowed for CORS.\n allow_headers: Headers allowed for CORS.\n ssl_certfile: SSL certificate file for HTTPS. See https://www.uvicorn.dev/settings/#https.\n ssl_keyfile: SSL key file for HTTPS. See https://www.uvicorn.dev/settings/#https.\n log_tokenization: Enable logging of parsed prompt tokens.\n patchmatch: Enable patchmatch inpaint code.\n models_dir: Path to the models directory.\n convert_cache_dir: Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions).\n download_cache_dir: Path to the directory that contains dynamically downloaded models.\n legacy_conf_dir: Path to directory of legacy checkpoint config files.\n db_dir: Path to InvokeAI databases directory.\n outputs_dir: Path to directory for outputs.\n 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`\n custom_nodes_dir: Path to directory for custom nodes.\n style_presets_dir: Path to directory for style presets.\n workflow_thumbnails_dir: Path to directory for workflow thumbnails.\n log_handlers: Log handler. Valid options are \"console\", \"file=\", \"syslog=path|address:host:port\", \"http=\".\n 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`\n log_level: Emit logging messages at this level or higher.
Valid values: `debug`, `info`, `warning`, `error`, `critical`\n log_sql: Log SQL queries. `log_level` must be `debug` for this to do anything. Extremely verbose.\n log_level_network: Log level for network-related messages. 'info' and 'debug' are very verbose.
Valid values: `debug`, `info`, `warning`, `error`, `critical`\n use_memory_db: Use in-memory database. Useful for development.\n dev_reload: Automatically reload when Python sources are changed. Does not reload node definitions.\n profile_graphs: Enable graph profiling using `cProfile`.\n profile_prefix: An optional prefix for profile output files.\n profiles_dir: Path to profiles output directory.\n 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.\n 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.\n 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.\n 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.\n 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.\n 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.\n 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.\n 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.\n 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.\n 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.\n 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.\n 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)\n 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`\n sequential_guidance: Whether to calculate guidance in serial instead of in parallel, lowering memory requirements.\n attention_type: Attention type.
Valid values: `auto`, `normal`, `xformers`, `sliced`, `torch-sdp`\n attention_slice_size: Slice size, valid when attention_type==\"sliced\".
Valid values: `auto`, `balanced`, `max`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`\n force_tiled_decode: Whether to enable tiled VAE decode (reduces memory consumption with some performance penalty).\n 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.\n max_queue_size: Maximum number of items in the session queue.\n clear_queue_on_startup: Empties session queue on startup. If true, disables `max_queue_history`.\n 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.\n allow_nodes: List of nodes to allow. Omit to allow all.\n deny_nodes: List of nodes to deny. Omit to deny none.\n node_cache_size: How many cached nodes to keep in memory.\n 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`\n 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.\n 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.\n 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.\n 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.\n 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.\n 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.\n external_alibabacloud_api_key: API key for Alibaba Cloud DashScope image generation.\n external_alibabacloud_base_url: Base URL override for Alibaba Cloud DashScope image generation.\n external_gemini_api_key: API key for Gemini image generation.\n external_openai_api_key: API key for OpenAI image generation.\n external_gemini_base_url: Base URL override for Gemini image generation.\n external_openai_base_url: Base URL override for OpenAI image generation.\n external_seedream_api_key: API key for Seedream image generation.\n external_seedream_base_url: Base URL override for Seedream image generation." + }, + "InvokeAIAppConfigWithSetFields": { + "properties": { + "set_fields": { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true, + "title": "Set Fields", + "description": "The set fields" + }, + "config": { + "$ref": "#/components/schemas/InvokeAIAppConfig", + "description": "The InvokeAI App Config" + } + }, + "type": "object", + "required": ["set_fields", "config"], + "title": "InvokeAIAppConfigWithSetFields", + "description": "InvokeAI App Config with model fields set" + }, + "InvokeAdjustImageHuePlusInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Adjusts the Hue of an image by rotating it in the selected color space. Originally created by @dwringer", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to adjust", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "space": { + "default": "HSV / HSL / RGB", + "description": "Color space in which to rotate hue by polar coords (*: non-invertible)", + "enum": [ + "HSV / HSL / RGB", + "Okhsl", + "Okhsv", + "*Oklch / Oklab", + "*LCh / CIELab", + "*UPLab (w/CIELab_to_UPLab.icc)" + ], + "field_kind": "input", + "input": "any", + "orig_default": "HSV / HSL / RGB", + "orig_required": false, + "title": "Space", + "type": "string" + }, + "degrees": { + "default": 0.0, + "description": "Degrees by which to rotate image hue", + "field_kind": "input", + "input": "any", + "orig_default": 0.0, + "orig_required": false, + "title": "Degrees", + "type": "number" + }, + "preserve_lightness": { + "default": false, + "description": "Whether to preserve CIELAB lightness values", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Preserve Lightness", + "type": "boolean" + }, + "ok_adaptive_gamut": { + "default": 0.05, + "description": "Higher preserves chroma at the expense of lightness (Oklab)", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 0.05, + "orig_required": false, + "title": "Ok Adaptive Gamut", + "type": "number" + }, + "ok_high_precision": { + "default": true, + "description": "Use more steps in computing gamut (Oklab/Okhsv/Okhsl)", + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "Ok High Precision", + "type": "boolean" + }, + "type": { + "const": "invokeai_img_hue_adjust_plus", + "default": "invokeai_img_hue_adjust_plus", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "hue", "oklab", "cielab", "uplab", "lch", "hsv", "hsl", "lab"], + "title": "Adjust Image Hue Plus", + "type": "object", + "version": "1.2.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "InvokeEquivalentAchromaticLightnessInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Calculate Equivalent Achromatic Lightness from image. Originally created by @dwringer", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Image from which to get channel", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "invokeai_ealightness", + "default": "invokeai_ealightness", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "channel", "mask", "cielab", "lab"], + "title": "Equivalent Achromatic Lightness", + "type": "object", + "version": "1.2.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "InvokeImageBlendInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Blend two images together, with optional opacity, mask, and blend modes. Originally created by @dwringer", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "layer_upper": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The top image to blend", + "field_kind": "input", + "input": "any", + "orig_required": true, + "ui_order": 1 + }, + "blend_mode": { + "default": "Normal", + "description": "Available blend modes", + "enum": [ + "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" + ], + "field_kind": "input", + "input": "any", + "orig_default": "Normal", + "orig_required": false, + "title": "Blend Mode", + "type": "string", + "ui_order": 2 + }, + "opacity": { + "default": 1.0, + "description": "Desired opacity of the upper layer", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Opacity", + "type": "number", + "ui_order": 3 + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional mask, used to restrict areas from blending", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "ui_order": 4 + }, + "fit_to_width": { + "default": false, + "description": "Scale upper layer to fit base width", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Fit To Width", + "type": "boolean", + "ui_order": 5 + }, + "fit_to_height": { + "default": true, + "description": "Scale upper layer to fit base height", + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "Fit To Height", + "type": "boolean", + "ui_order": 6 + }, + "layer_base": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The bottom image to blend", + "field_kind": "input", + "input": "any", + "orig_required": true, + "ui_order": 7 + }, + "color_space": { + "default": "RGB", + "description": "Available color spaces for blend computations", + "enum": ["RGB", "Linear RGB", "HSL (RGB)", "HSV (RGB)", "Okhsl", "Okhsv", "Oklch (Oklab)", "LCh (CIELab)"], + "field_kind": "input", + "input": "any", + "orig_default": "RGB", + "orig_required": false, + "title": "Color Space", + "type": "string", + "ui_order": 8 + }, + "adaptive_gamut": { + "default": 0.0, + "description": "Adaptive gamut clipping (0=off). Higher prioritizes chroma over lightness", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 0.0, + "orig_required": false, + "title": "Adaptive Gamut", + "type": "number", + "ui_order": 9 + }, + "high_precision": { + "default": true, + "description": "Use more steps in computing gamut when possible", + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "High Precision", + "type": "boolean", + "ui_order": 10 + }, + "type": { + "const": "invokeai_img_blend", + "default": "invokeai_img_blend", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "blend", "layer", "alpha", "composite", "dodge", "burn"], + "title": "Image Layer Blend", + "type": "object", + "version": "1.2.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "InvokeImageCompositorInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Removes backdrop from subject image then overlays subject on background image. Originally created by @dwringer", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image_subject": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Image of the subject on a plain monochrome background", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "image_background": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Image of a background scene", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "chroma_key": { + "default": "", + "description": "Can be empty for corner flood select, or CSS-3 color or tuple", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Chroma Key", + "type": "string" + }, + "threshold": { + "default": 50, + "description": "Subject isolation flood-fill threshold", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 50, + "orig_required": false, + "title": "Threshold", + "type": "integer" + }, + "fill_x": { + "default": false, + "description": "Scale base subject image to fit background width", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Fill X", + "type": "boolean" + }, + "fill_y": { + "default": true, + "description": "Scale base subject image to fit background height", + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "Fill Y", + "type": "boolean" + }, + "x_offset": { + "default": 0, + "description": "x-offset for the subject", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "X Offset", + "type": "integer" + }, + "y_offset": { + "default": 0, + "description": "y-offset for the subject", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Y Offset", + "type": "integer" + }, + "type": { + "const": "invokeai_img_composite", + "default": "invokeai_img_composite", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "compose", "chroma", "key"], + "title": "Image Compositor", + "type": "object", + "version": "1.2.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "InvokeImageDilateOrErodeInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Dilate (expand) or erode (contract) an image. Originally created by @dwringer", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image from which to create a mask", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "lightness_only": { + "default": false, + "description": "If true, only applies to image lightness (CIELa*b*)", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Lightness Only", + "type": "boolean" + }, + "radius_w": { + "default": 4, + "description": "Width (in pixels) by which to dilate(expand) or erode (contract) the image", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 4, + "orig_required": false, + "title": "Radius W", + "type": "integer" + }, + "radius_h": { + "default": 4, + "description": "Height (in pixels) by which to dilate(expand) or erode (contract) the image", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 4, + "orig_required": false, + "title": "Radius H", + "type": "integer" + }, + "mode": { + "default": "Dilate", + "description": "How to operate on the image", + "enum": ["Dilate", "Erode"], + "field_kind": "input", + "input": "any", + "orig_default": "Dilate", + "orig_required": false, + "title": "Mode", + "type": "string" + }, + "type": { + "const": "invokeai_img_dilate_erode", + "default": "invokeai_img_dilate_erode", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "mask", "dilate", "erode", "expand", "contract", "mask"], + "title": "Image Dilate or Erode", + "type": "object", + "version": "1.3.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "InvokeImageEnhanceInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Applies processing from PIL's ImageEnhance module. Originally created by @dwringer", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image for which to apply processing", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "invert": { + "default": false, + "description": "Whether to invert the image colors", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Invert", + "type": "boolean" + }, + "color": { + "default": 1.0, + "description": "Color enhancement factor", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Color", + "type": "number" + }, + "contrast": { + "default": 1.0, + "description": "Contrast enhancement factor", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Contrast", + "type": "number" + }, + "brightness": { + "default": 1.0, + "description": "Brightness enhancement factor", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Brightness", + "type": "number" + }, + "sharpness": { + "default": 1.0, + "description": "Sharpness enhancement factor", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Sharpness", + "type": "number" + }, + "type": { + "const": "invokeai_img_enhance", + "default": "invokeai_img_enhance", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["enhance", "image"], + "title": "Enhance Image", + "type": "object", + "version": "1.2.1", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "InvokeImageValueThresholdsInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Clip image to pure black/white past specified thresholds. Originally created by @dwringer", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image from which to create a mask", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "invert_output": { + "default": false, + "description": "Make light areas dark and vice versa", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Invert Output", + "type": "boolean" + }, + "renormalize_values": { + "default": false, + "description": "Rescale remaining values from minimum to maximum", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Renormalize Values", + "type": "boolean" + }, + "lightness_only": { + "default": false, + "description": "If true, only applies to image lightness (CIELa*b*)", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Lightness Only", + "type": "boolean" + }, + "threshold_upper": { + "default": 0.5, + "description": "Threshold above which will be set to full value", + "field_kind": "input", + "input": "any", + "orig_default": 0.5, + "orig_required": false, + "title": "Threshold Upper", + "type": "number" + }, + "threshold_lower": { + "default": 0.5, + "description": "Threshold below which will be set to minimum value", + "field_kind": "input", + "input": "any", + "orig_default": 0.5, + "orig_required": false, + "title": "Threshold Lower", + "type": "number" + }, + "type": { + "const": "invokeai_img_val_thresholds", + "default": "invokeai_img_val_thresholds", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "mask", "value", "threshold"], + "title": "Image Value Thresholds", + "type": "object", + "version": "1.2.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ItemIdsResult": { + "properties": { + "item_ids": { + "items": { + "type": "integer" + }, + "type": "array", + "title": "Item Ids", + "description": "Ordered list of item ids" + }, + "total_count": { + "type": "integer", + "title": "Total Count", + "description": "Total number of queue items matching the query" + } + }, + "type": "object", + "required": ["item_ids", "total_count"], + "title": "ItemIdsResult", + "description": "Response containing ordered item ids with metadata for optimistic updates." + }, + "IterateInvocation": { + "class": "invocation", + "classification": "stable", + "description": "Iterates over a list of items", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "collection": { + "default": [], + "description": "The list of items to iterate over", + "field_kind": "input", + "input": "any", + "items": {}, + "orig_default": [], + "orig_required": false, + "title": "Collection", + "type": "array", + "ui_type": "CollectionField" + }, + "index": { + "default": 0, + "description": "The index, will be provided on executed iterators", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Index", + "type": "integer", + "ui_hidden": true + }, + "type": { + "const": "iterate", + "default": "iterate", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "title": "IterateInvocation", + "type": "object", + "version": "1.1.0", + "output": { + "$ref": "#/components/schemas/IterateInvocationOutput" + } + }, + "IterateInvocationOutput": { + "class": "output", + "description": "Used to connect iteration outputs. Will be expanded to a specific output.", + "properties": { + "item": { + "description": "The item being iterated over", + "field_kind": "output", + "title": "Collection Item", + "ui_hidden": false, + "ui_type": "CollectionItemField" + }, + "index": { + "description": "The index of the item", + "field_kind": "output", + "title": "Index", + "type": "integer", + "ui_hidden": false + }, + "total": { + "description": "The total number of items", + "field_kind": "output", + "title": "Total", + "type": "integer", + "ui_hidden": false + }, + "type": { + "const": "iterate_output", + "default": "iterate_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "item", "index", "total", "type", "type"], + "title": "IterateInvocationOutput", + "type": "object" + }, + "JsonValue": {}, + "LaMaInfillInvocation": { + "category": "inpaint", + "class": "invocation", + "classification": "stable", + "description": "Infills transparent areas of an image using the LaMa model", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "infill_lama", + "default": "infill_lama", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "inpaint"], + "title": "LaMa Infill", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "LatentsCollectionInvocation": { + "category": "primitives", + "class": "invocation", + "classification": "stable", + "description": "A collection of latents tensor primitive values", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "collection": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/LatentsField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The collection of latents tensors", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Collection" + }, + "type": { + "const": "latents_collection", + "default": "latents_collection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "latents", "collection"], + "title": "Latents Collection Primitive", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/LatentsCollectionOutput" + } + }, + "LatentsCollectionOutput": { + "class": "output", + "description": "Base class for nodes that output a collection of latents tensors", + "properties": { + "collection": { + "description": "Latents tensor", + "field_kind": "output", + "items": { + "$ref": "#/components/schemas/LatentsField" + }, + "title": "Collection", + "type": "array", + "ui_hidden": false + }, + "type": { + "const": "latents_collection_output", + "default": "latents_collection_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "collection", "type", "type"], + "title": "LatentsCollectionOutput", + "type": "object" + }, + "LatentsField": { + "description": "A latents tensor primitive field", + "properties": { + "latents_name": { + "description": "The name of the latents", + "title": "Latents Name", + "type": "string" + }, + "seed": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Seed used to generate this latents", + "title": "Seed" + } + }, + "required": ["latents_name"], + "title": "LatentsField", + "type": "object" + }, + "LatentsInvocation": { + "category": "primitives", + "class": "invocation", + "classification": "stable", + "description": "A latents tensor primitive value", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The latents tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "type": { + "const": "latents", + "default": "latents", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "latents"], + "title": "Latents Primitive", + "type": "object", + "version": "1.0.2", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "LatentsMetaOutput": { + "class": "output", + "description": "Latents + metadata", + "properties": { + "metadata": { + "$ref": "#/components/schemas/MetadataField", + "description": "Metadata Dict", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "latents_meta_output", + "default": "latents_meta_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + }, + "latents": { + "$ref": "#/components/schemas/LatentsField", + "description": "Latents tensor", + "field_kind": "output", + "ui_hidden": false + }, + "width": { + "description": "Width of output (px)", + "field_kind": "output", + "title": "Width", + "type": "integer", + "ui_hidden": false + }, + "height": { + "description": "Height of output (px)", + "field_kind": "output", + "title": "Height", + "type": "integer", + "ui_hidden": false + } + }, + "required": ["output_meta", "metadata", "type", "latents", "width", "height", "type"], + "title": "LatentsMetaOutput", + "type": "object" + }, + "LatentsOutput": { + "class": "output", + "description": "Base class for nodes that output a single latents tensor", + "properties": { + "latents": { + "$ref": "#/components/schemas/LatentsField", + "description": "Latents tensor", + "field_kind": "output", + "ui_hidden": false + }, + "width": { + "description": "Width of output (px)", + "field_kind": "output", + "title": "Width", + "type": "integer", + "ui_hidden": false + }, + "height": { + "description": "Height of output (px)", + "field_kind": "output", + "title": "Height", + "type": "integer", + "ui_hidden": false + }, + "type": { + "const": "latents_output", + "default": "latents_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "latents", "width", "height", "type", "type"], + "title": "LatentsOutput", + "type": "object" + }, + "LatentsToImageInvocation": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Generates an image from latents.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "tiled": { + "default": false, + "description": "Processing using overlapping tiles (reduce memory consumption)", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Tiled", + "type": "boolean" + }, + "tile_size": { + "default": 0, + "description": "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.", + "field_kind": "input", + "input": "any", + "multipleOf": 8, + "orig_default": 0, + "orig_required": false, + "title": "Tile Size", + "type": "integer" + }, + "fp32": { + "default": false, + "description": "Whether or not to use full float32 precision", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Fp32", + "type": "boolean" + }, + "type": { + "const": "l2i", + "default": "l2i", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "image", "vae", "l2i"], + "title": "Latents to Image - SD1.5, SDXL", + "type": "object", + "version": "1.3.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "LineartAnimeEdgeDetectionInvocation": { + "category": "controlnet_preprocessors", + "class": "invocation", + "classification": "stable", + "description": "Geneartes an edge map using the Lineart model.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "lineart_anime_edge_detection", + "default": "lineart_anime_edge_detection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["controlnet", "lineart"], + "title": "Lineart Anime Edge Detection", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "LineartEdgeDetectionInvocation": { + "category": "controlnet_preprocessors", + "class": "invocation", + "classification": "stable", + "description": "Generates an edge map using the Lineart model.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "coarse": { + "default": false, + "description": "Whether to use coarse mode", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Coarse", + "type": "boolean" + }, + "type": { + "const": "lineart_edge_detection", + "default": "lineart_edge_detection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["controlnet", "lineart"], + "title": "Lineart Edge Detection", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "LlavaOnevisionVllmInvocation": { + "category": "multimodal", + "class": "invocation", + "classification": "beta", + "description": "Run a LLaVA OneVision VLLM model.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "images": { + "anyOf": [ + { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ImageField" + }, + "type": "array" + }, + { + "$ref": "#/components/schemas/ImageField" + } + ], + "maxLength": 3 + }, + { + "type": "null" + } + ], + "default": null, + "description": "Input image.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Images" + }, + "prompt": { + "default": "", + "description": "Input text prompt.", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Prompt", + "type": "string", + "ui_component": "textarea" + }, + "vllm_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The VLLM model to use", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "LLaVA Model Type", + "ui_model_type": ["llava_onevision"] + }, + "type": { + "const": "llava_onevision_vllm", + "default": "llava_onevision_vllm", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["vllm"], + "title": "LLaVA OneVision VLLM", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/StringOutput" + } + }, + "LlavaOnevision_Diffusers_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "type": { + "type": "string", + "const": "llava_onevision", + "title": "Type", + "default": "llava_onevision" + }, + "base": { + "type": "string", + "const": "any", + "title": "Base", + "default": "any" + }, + "cpu_only": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Cpu Only", + "description": "Whether this model should run on CPU only" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "format", + "repo_variant", + "type", + "base", + "cpu_only" + ], + "title": "LlavaOnevision_Diffusers_Config", + "description": "Model config for Llava Onevision models." + }, + "LoRACollectionLoader": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Applies a collection of LoRAs to the provided UNet and CLIP models.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "loras": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoRAField" + }, + { + "items": { + "$ref": "#/components/schemas/LoRAField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LoRA models and weights. May be a single LoRA or collection.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "LoRAs" + }, + "unet": { + "anyOf": [ + { + "$ref": "#/components/schemas/UNetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "UNet (scheduler, LoRAs)", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "UNet" + }, + "clip": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "CLIP" + }, + "type": { + "const": "lora_collection_loader", + "default": "lora_collection_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["model"], + "title": "Apply LoRA Collection - SD1.5", + "type": "object", + "version": "1.1.2", + "output": { + "$ref": "#/components/schemas/LoRALoaderOutput" + } + }, + "LoRAField": { + "properties": { + "lora": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Info to load lora model" + }, + "weight": { + "description": "Weight to apply to lora model", + "title": "Weight", + "type": "number" + } + }, + "required": ["lora", "weight"], + "title": "LoRAField", + "type": "object" + }, + "LoRALoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Apply selected lora to unet and text_encoder.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "lora": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LoRA model to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "LoRA", + "ui_model_base": ["sd-1"], + "ui_model_type": ["lora"] + }, + "weight": { + "default": 0.75, + "description": "The weight at which the LoRA is applied to each model", + "field_kind": "input", + "input": "any", + "orig_default": 0.75, + "orig_required": false, + "title": "Weight", + "type": "number" + }, + "unet": { + "anyOf": [ + { + "$ref": "#/components/schemas/UNetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "UNet (scheduler, LoRAs)", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "UNet" + }, + "clip": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "CLIP" + }, + "type": { + "const": "lora_loader", + "default": "lora_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["model"], + "title": "Apply LoRA - SD1.5", + "type": "object", + "version": "1.0.4", + "output": { + "$ref": "#/components/schemas/LoRALoaderOutput" + } + }, + "LoRALoaderOutput": { + "class": "output", + "description": "Model loader output", + "properties": { + "unet": { + "anyOf": [ + { + "$ref": "#/components/schemas/UNetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "UNet (scheduler, LoRAs)", + "field_kind": "output", + "title": "UNet", + "ui_hidden": false + }, + "clip": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "output", + "title": "CLIP", + "ui_hidden": false + }, + "type": { + "const": "lora_loader_output", + "default": "lora_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "unet", "clip", "type", "type"], + "title": "LoRALoaderOutput", + "type": "object" + }, + "LoRAMetadataField": { + "description": "LoRA Metadata Field", + "properties": { + "model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "LoRA model to load" + }, + "weight": { + "description": "The weight at which the LoRA is applied to each model", + "title": "Weight", + "type": "number" + } + }, + "required": ["model", "weight"], + "title": "LoRAMetadataField", + "type": "object" + }, + "LoRARecallParameter": { + "properties": { + "model_name": { + "type": "string", + "title": "Model Name", + "description": "The name of the LoRA model" + }, + "weight": { + "type": "number", + "maximum": 10.0, + "minimum": -10.0, + "title": "Weight", + "description": "The weight for the LoRA", + "default": 0.75 + }, + "is_enabled": { + "type": "boolean", + "title": "Is Enabled", + "description": "Whether the LoRA is enabled", + "default": true + } + }, + "type": "object", + "required": ["model_name"], + "title": "LoRARecallParameter", + "description": "LoRA configuration for recall" + }, + "LoRASelectorInvocation": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Selects a LoRA model and weight.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "lora": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LoRA model to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "LoRA", + "ui_model_type": ["lora"] + }, + "weight": { + "default": 0.75, + "description": "The weight at which the LoRA is applied to each model", + "field_kind": "input", + "input": "any", + "orig_default": 0.75, + "orig_required": false, + "title": "Weight", + "type": "number" + }, + "type": { + "const": "lora_selector", + "default": "lora_selector", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["model"], + "title": "Select LoRA", + "type": "object", + "version": "1.0.3", + "output": { + "$ref": "#/components/schemas/LoRASelectorOutput" + } + }, + "LoRASelectorOutput": { + "class": "output", + "description": "Model loader output", + "properties": { + "lora": { + "$ref": "#/components/schemas/LoRAField", + "description": "LoRA model and weight", + "field_kind": "output", + "title": "LoRA", + "ui_hidden": false + }, + "type": { + "const": "lora_selector_output", + "default": "lora_selector_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "lora", "type", "type"], + "title": "LoRASelectorOutput", + "type": "object" + }, + "LoRA_Diffusers_FLUX_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "lora", + "title": "Type", + "default": "lora" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "base": { + "type": "string", + "const": "flux", + "title": "Base", + "default": "flux" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "base" + ], + "title": "LoRA_Diffusers_FLUX_Config" + }, + "LoRA_Diffusers_Flux2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "lora", + "title": "Type", + "default": "lora" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "base": { + "type": "string", + "const": "flux2", + "title": "Base", + "default": "flux2" + }, + "variant": { + "anyOf": [ + { + "$ref": "#/components/schemas/Flux2VariantType" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "base", + "variant" + ], + "title": "LoRA_Diffusers_Flux2_Config", + "description": "Model config for FLUX.2 (Klein) LoRA models in Diffusers format." + }, + "LoRA_Diffusers_SD1_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "lora", + "title": "Type", + "default": "lora" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "base": { + "type": "string", + "const": "sd-1", + "title": "Base", + "default": "sd-1" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "base" + ], + "title": "LoRA_Diffusers_SD1_Config" + }, + "LoRA_Diffusers_SD2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "lora", + "title": "Type", + "default": "lora" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "base": { + "type": "string", + "const": "sd-2", + "title": "Base", + "default": "sd-2" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "base" + ], + "title": "LoRA_Diffusers_SD2_Config" + }, + "LoRA_Diffusers_SDXL_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "lora", + "title": "Type", + "default": "lora" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "base": { + "type": "string", + "const": "sdxl", + "title": "Base", + "default": "sdxl" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "base" + ], + "title": "LoRA_Diffusers_SDXL_Config" + }, + "LoRA_Diffusers_ZImage_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "lora", + "title": "Type", + "default": "lora" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "base": { + "type": "string", + "const": "z-image", + "title": "Base", + "default": "z-image" + }, + "variant": { + "anyOf": [ + { + "$ref": "#/components/schemas/ZImageVariantType" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "base", + "variant" + ], + "title": "LoRA_Diffusers_ZImage_Config", + "description": "Model config for Z-Image LoRA models in Diffusers format." + }, + "LoRA_LyCORIS_Anima_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "lora", + "title": "Type", + "default": "lora" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "lycoris", + "title": "Format", + "default": "lycoris" + }, + "base": { + "type": "string", + "const": "anima", + "title": "Base", + "default": "anima" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "base" + ], + "title": "LoRA_LyCORIS_Anima_Config", + "description": "Model config for Anima LoRA models in LyCORIS format." + }, + "LoRA_LyCORIS_FLUX_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "lora", + "title": "Type", + "default": "lora" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "lycoris", + "title": "Format", + "default": "lycoris" + }, + "base": { + "type": "string", + "const": "flux", + "title": "Base", + "default": "flux" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "base" + ], + "title": "LoRA_LyCORIS_FLUX_Config" + }, + "LoRA_LyCORIS_Flux2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "lora", + "title": "Type", + "default": "lora" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "lycoris", + "title": "Format", + "default": "lycoris" + }, + "base": { + "type": "string", + "const": "flux2", + "title": "Base", + "default": "flux2" + }, + "variant": { + "anyOf": [ + { + "$ref": "#/components/schemas/Flux2VariantType" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "base", + "variant" + ], + "title": "LoRA_LyCORIS_Flux2_Config", + "description": "Model config for FLUX.2 (Klein) LoRA models in LyCORIS format." + }, + "LoRA_LyCORIS_QwenImage_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "lora", + "title": "Type", + "default": "lora" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "lycoris", + "title": "Format", + "default": "lycoris" + }, + "base": { + "type": "string", + "const": "qwen-image", + "title": "Base", + "default": "qwen-image" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "base" + ], + "title": "LoRA_LyCORIS_QwenImage_Config", + "description": "Model config for Qwen Image Edit LoRA models in LyCORIS format." + }, + "LoRA_LyCORIS_SD1_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "lora", + "title": "Type", + "default": "lora" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "lycoris", + "title": "Format", + "default": "lycoris" + }, + "base": { + "type": "string", + "const": "sd-1", + "title": "Base", + "default": "sd-1" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "base" + ], + "title": "LoRA_LyCORIS_SD1_Config" + }, + "LoRA_LyCORIS_SD2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "lora", + "title": "Type", + "default": "lora" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "lycoris", + "title": "Format", + "default": "lycoris" + }, + "base": { + "type": "string", + "const": "sd-2", + "title": "Base", + "default": "sd-2" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "base" + ], + "title": "LoRA_LyCORIS_SD2_Config" + }, + "LoRA_LyCORIS_SDXL_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "lora", + "title": "Type", + "default": "lora" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "lycoris", + "title": "Format", + "default": "lycoris" + }, + "base": { + "type": "string", + "const": "sdxl", + "title": "Base", + "default": "sdxl" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "base" + ], + "title": "LoRA_LyCORIS_SDXL_Config" + }, + "LoRA_LyCORIS_ZImage_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "lora", + "title": "Type", + "default": "lora" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "lycoris", + "title": "Format", + "default": "lycoris" + }, + "base": { + "type": "string", + "const": "z-image", + "title": "Base", + "default": "z-image" + }, + "variant": { + "anyOf": [ + { + "$ref": "#/components/schemas/ZImageVariantType" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "base", + "variant" + ], + "title": "LoRA_LyCORIS_ZImage_Config", + "description": "Model config for Z-Image LoRA models in LyCORIS format." + }, + "LoRA_OMI_FLUX_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "lora", + "title": "Type", + "default": "lora" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "omi", + "title": "Format", + "default": "omi" + }, + "base": { + "type": "string", + "const": "flux", + "title": "Base", + "default": "flux" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "base" + ], + "title": "LoRA_OMI_FLUX_Config" + }, + "LoRA_OMI_SDXL_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "lora", + "title": "Type", + "default": "lora" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "omi", + "title": "Format", + "default": "omi" + }, + "base": { + "type": "string", + "const": "sdxl", + "title": "Base", + "default": "sdxl" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "base" + ], + "title": "LoRA_OMI_SDXL_Config" + }, + "LocalModelSource": { + "properties": { + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "path" + } + ], + "title": "Path" + }, + "inplace": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Inplace", + "default": false + }, + "type": { + "type": "string", + "const": "local", + "title": "Type", + "default": "local" + } + }, + "type": "object", + "required": ["path"], + "title": "LocalModelSource", + "description": "A local file or directory path." + }, + "LogLevel": { + "type": "integer", + "enum": [0, 10, 20, 30, 40, 50], + "title": "LogLevel" + }, + "LoginRequest": { + "properties": { + "email": { + "type": "string", + "title": "Email", + "description": "User email address" + }, + "password": { + "type": "string", + "title": "Password", + "description": "User password" + }, + "remember_me": { + "type": "boolean", + "title": "Remember Me", + "description": "Whether to extend session duration", + "default": false + } + }, + "type": "object", + "required": ["email", "password"], + "title": "LoginRequest", + "description": "Request body for user login." + }, + "LoginResponse": { + "properties": { + "token": { + "type": "string", + "title": "Token", + "description": "JWT access token" + }, + "user": { + "$ref": "#/components/schemas/UserDTO", + "description": "User information" + }, + "expires_in": { + "type": "integer", + "title": "Expires In", + "description": "Token expiration time in seconds" + } + }, + "type": "object", + "required": ["token", "user", "expires_in"], + "title": "LoginResponse", + "description": "Response from successful login." + }, + "LogoutResponse": { + "properties": { + "success": { + "type": "boolean", + "title": "Success", + "description": "Whether logout was successful" + } + }, + "type": "object", + "required": ["success"], + "title": "LogoutResponse", + "description": "Response from logout." + }, + "LoraModelDefaultSettings": { + "properties": { + "weight": { + "anyOf": [ + { + "type": "number", + "maximum": 2.0, + "minimum": -1.0 + }, + { + "type": "null" + } + ], + "title": "Weight", + "description": "Default weight for this model" + } + }, + "additionalProperties": false, + "type": "object", + "title": "LoraModelDefaultSettings" + }, + "MDControlListOutput": { + "class": "output", + "properties": { + "control_list": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlField" + }, + { + "items": { + "$ref": "#/components/schemas/ControlField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "ControlNet(s) to apply", + "field_kind": "output", + "title": "ControlNet-List", + "ui_hidden": false + }, + "type": { + "const": "md_control_list_output", + "default": "md_control_list_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "control_list", "type", "type"], + "title": "MDControlListOutput", + "type": "object" + }, + "MDIPAdapterListOutput": { + "class": "output", + "properties": { + "ip_adapter_list": { + "anyOf": [ + { + "$ref": "#/components/schemas/IPAdapterField" + }, + { + "items": { + "$ref": "#/components/schemas/IPAdapterField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "IP-Adapter to apply", + "field_kind": "output", + "title": "IP-Adapter-List", + "ui_hidden": false + }, + "type": { + "const": "md_ip_adapter_list_output", + "default": "md_ip_adapter_list_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "ip_adapter_list", "type", "type"], + "title": "MDIPAdapterListOutput", + "type": "object" + }, + "MDT2IAdapterListOutput": { + "class": "output", + "properties": { + "t2i_adapter_list": { + "anyOf": [ + { + "$ref": "#/components/schemas/T2IAdapterField" + }, + { + "items": { + "$ref": "#/components/schemas/T2IAdapterField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "T2I-Adapter(s) to apply", + "field_kind": "output", + "title": "T2I Adapter-List", + "ui_hidden": false + }, + "type": { + "const": "md_ip_adapters_output", + "default": "md_ip_adapters_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "t2i_adapter_list", "type", "type"], + "title": "MDT2IAdapterListOutput", + "type": "object" + }, + "MLSDDetectionInvocation": { + "category": "controlnet_preprocessors", + "class": "invocation", + "classification": "stable", + "description": "Generates an line segment map using MLSD.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "score_threshold": { + "default": 0.1, + "description": "The threshold used to score points when determining line segments", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 0.1, + "orig_required": false, + "title": "Score Threshold", + "type": "number" + }, + "distance_threshold": { + "default": 20.0, + "description": "Threshold for including a line segment - lines shorter than this distance will be discarded", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 20.0, + "orig_required": false, + "title": "Distance Threshold", + "type": "number" + }, + "type": { + "const": "mlsd_detection", + "default": "mlsd_detection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["controlnet", "mlsd", "edge"], + "title": "MLSD Detection", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "MainModelDefaultSettings": { + "properties": { + "vae": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vae", + "description": "Default VAE for this model (model key)" + }, + "vae_precision": { + "anyOf": [ + { + "type": "string", + "enum": ["fp16", "fp32"] + }, + { + "type": "null" + } + ], + "title": "Vae Precision", + "description": "Default VAE precision for this model" + }, + "scheduler": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ddim", + "ddpm", + "deis", + "deis_k", + "lms", + "lms_k", + "pndm", + "heun", + "heun_k", + "euler", + "euler_k", + "euler_a", + "kdpm_2", + "kdpm_2_k", + "kdpm_2_a", + "kdpm_2_a_k", + "dpmpp_2s", + "dpmpp_2s_k", + "dpmpp_2m", + "dpmpp_2m_k", + "dpmpp_2m_sde", + "dpmpp_2m_sde_k", + "dpmpp_3m", + "dpmpp_3m_k", + "dpmpp_sde", + "dpmpp_sde_k", + "er_sde", + "unipc", + "unipc_k", + "lcm", + "tcd" + ] + }, + { + "type": "null" + } + ], + "title": "Scheduler", + "description": "Default scheduler for this model" + }, + "steps": { + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Steps", + "description": "Default number of steps for this model" + }, + "cfg_scale": { + "anyOf": [ + { + "type": "number", + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Cfg Scale", + "description": "Default CFG Scale for this model" + }, + "cfg_rescale_multiplier": { + "anyOf": [ + { + "type": "number", + "exclusiveMaximum": 1.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Cfg Rescale Multiplier", + "description": "Default CFG Rescale Multiplier for this model" + }, + "width": { + "anyOf": [ + { + "type": "integer", + "multipleOf": 8.0, + "minimum": 64.0 + }, + { + "type": "null" + } + ], + "title": "Width", + "description": "Default width for this model" + }, + "height": { + "anyOf": [ + { + "type": "integer", + "multipleOf": 8.0, + "minimum": 64.0 + }, + { + "type": "null" + } + ], + "title": "Height", + "description": "Default height for this model" + }, + "guidance": { + "anyOf": [ + { + "type": "number", + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Guidance", + "description": "Default Guidance for this model" + }, + "cpu_only": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Cpu Only", + "description": "Whether this model should run on CPU only" + }, + "fp8_storage": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Fp8 Storage", + "description": "Store weights in FP8 to reduce VRAM usage (~50% savings). Weights are cast to compute dtype during inference." + } + }, + "additionalProperties": false, + "type": "object", + "title": "MainModelDefaultSettings" + }, + "MainModelLoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Loads a main model, outputting its submodels.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Main model (UNet, VAE, CLIP) to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "ui_model_base": ["sd-1", "sd-2"], + "ui_model_type": ["main"] + }, + "type": { + "const": "main_model_loader", + "default": "main_model_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["model"], + "title": "Main Model - SD1.5, SD2", + "type": "object", + "version": "1.0.4", + "output": { + "$ref": "#/components/schemas/ModelLoaderOutput" + } + }, + "Main_BnBNF4_FLUX_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "base": { + "type": "string", + "const": "flux", + "title": "Base", + "default": "flux" + }, + "format": { + "type": "string", + "const": "bnb_quantized_nf4b", + "title": "Format", + "default": "bnb_quantized_nf4b" + }, + "variant": { + "$ref": "#/components/schemas/FluxVariantType" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "config_path", + "base", + "format", + "variant" + ], + "title": "Main_BnBNF4_FLUX_Config", + "description": "Model config for main checkpoint models." + }, + "Main_Checkpoint_Anima_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "base": { + "type": "string", + "const": "anima", + "title": "Base", + "default": "anima" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "config_path", + "base", + "format" + ], + "title": "Main_Checkpoint_Anima_Config", + "description": "Model config for Anima single-file checkpoint models (safetensors).\n\nAnima is built on NVIDIA Cosmos Predict2 DiT with a custom LLM Adapter\nthat bridges Qwen3 0.6B text encoder outputs to the DiT." + }, + "Main_Checkpoint_FLUX_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "base": { + "type": "string", + "const": "flux", + "title": "Base", + "default": "flux" + }, + "variant": { + "$ref": "#/components/schemas/FluxVariantType" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "config_path", + "format", + "base", + "variant" + ], + "title": "Main_Checkpoint_FLUX_Config", + "description": "Model config for main checkpoint models." + }, + "Main_Checkpoint_Flux2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "base": { + "type": "string", + "const": "flux2", + "title": "Base", + "default": "flux2" + }, + "variant": { + "$ref": "#/components/schemas/Flux2VariantType" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "config_path", + "format", + "base", + "variant" + ], + "title": "Main_Checkpoint_Flux2_Config", + "description": "Model config for FLUX.2 checkpoint models (e.g. Klein)." + }, + "Main_Checkpoint_SD1_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "prediction_type": { + "$ref": "#/components/schemas/SchedulerPredictionType" + }, + "variant": { + "$ref": "#/components/schemas/ModelVariantType" + }, + "base": { + "type": "string", + "const": "sd-1", + "title": "Base", + "default": "sd-1" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "config_path", + "format", + "prediction_type", + "variant", + "base" + ], + "title": "Main_Checkpoint_SD1_Config" + }, + "Main_Checkpoint_SD2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "prediction_type": { + "$ref": "#/components/schemas/SchedulerPredictionType" + }, + "variant": { + "$ref": "#/components/schemas/ModelVariantType" + }, + "base": { + "type": "string", + "const": "sd-2", + "title": "Base", + "default": "sd-2" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "config_path", + "format", + "prediction_type", + "variant", + "base" + ], + "title": "Main_Checkpoint_SD2_Config" + }, + "Main_Checkpoint_SDXLRefiner_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "prediction_type": { + "$ref": "#/components/schemas/SchedulerPredictionType" + }, + "variant": { + "$ref": "#/components/schemas/ModelVariantType" + }, + "base": { + "type": "string", + "const": "sdxl-refiner", + "title": "Base", + "default": "sdxl-refiner" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "config_path", + "format", + "prediction_type", + "variant", + "base" + ], + "title": "Main_Checkpoint_SDXLRefiner_Config" + }, + "Main_Checkpoint_SDXL_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "prediction_type": { + "$ref": "#/components/schemas/SchedulerPredictionType" + }, + "variant": { + "$ref": "#/components/schemas/ModelVariantType" + }, + "base": { + "type": "string", + "const": "sdxl", + "title": "Base", + "default": "sdxl" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "config_path", + "format", + "prediction_type", + "variant", + "base" + ], + "title": "Main_Checkpoint_SDXL_Config" + }, + "Main_Checkpoint_ZImage_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "base": { + "type": "string", + "const": "z-image", + "title": "Base", + "default": "z-image" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "variant": { + "$ref": "#/components/schemas/ZImageVariantType" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "config_path", + "base", + "format", + "variant" + ], + "title": "Main_Checkpoint_ZImage_Config", + "description": "Model config for Z-Image single-file checkpoint models (safetensors, etc)." + }, + "Main_Diffusers_CogView4_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "base": { + "type": "string", + "const": "cogview4", + "title": "Base", + "default": "cogview4" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "repo_variant", + "base" + ], + "title": "Main_Diffusers_CogView4_Config" + }, + "Main_Diffusers_FLUX_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "base": { + "type": "string", + "const": "flux", + "title": "Base", + "default": "flux" + }, + "variant": { + "$ref": "#/components/schemas/FluxVariantType" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "repo_variant", + "base", + "variant" + ], + "title": "Main_Diffusers_FLUX_Config", + "description": "Model config for FLUX.1 models in diffusers format." + }, + "Main_Diffusers_Flux2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "base": { + "type": "string", + "const": "flux2", + "title": "Base", + "default": "flux2" + }, + "variant": { + "$ref": "#/components/schemas/Flux2VariantType" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "repo_variant", + "base", + "variant" + ], + "title": "Main_Diffusers_Flux2_Config", + "description": "Model config for FLUX.2 models in diffusers format (e.g. FLUX.2 Klein)." + }, + "Main_Diffusers_QwenImage_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "base": { + "type": "string", + "const": "qwen-image", + "title": "Base", + "default": "qwen-image" + }, + "variant": { + "anyOf": [ + { + "$ref": "#/components/schemas/QwenImageVariantType" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "repo_variant", + "base", + "variant" + ], + "title": "Main_Diffusers_QwenImage_Config", + "description": "Model config for Qwen Image diffusers models (both txt2img and edit)." + }, + "Main_Diffusers_SD1_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "prediction_type": { + "$ref": "#/components/schemas/SchedulerPredictionType" + }, + "variant": { + "$ref": "#/components/schemas/ModelVariantType" + }, + "base": { + "type": "string", + "const": "sd-1", + "title": "Base", + "default": "sd-1" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "repo_variant", + "prediction_type", + "variant", + "base" + ], + "title": "Main_Diffusers_SD1_Config" + }, + "Main_Diffusers_SD2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "prediction_type": { + "$ref": "#/components/schemas/SchedulerPredictionType" + }, + "variant": { + "$ref": "#/components/schemas/ModelVariantType" + }, + "base": { + "type": "string", + "const": "sd-2", + "title": "Base", + "default": "sd-2" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "repo_variant", + "prediction_type", + "variant", + "base" + ], + "title": "Main_Diffusers_SD2_Config" + }, + "Main_Diffusers_SD3_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "base": { + "type": "string", + "const": "sd-3", + "title": "Base", + "default": "sd-3" + }, + "submodels": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/SubmodelDefinition" + }, + "propertyNames": { + "$ref": "#/components/schemas/SubModelType" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Submodels", + "description": "Loadable submodels in this model" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "repo_variant", + "base", + "submodels" + ], + "title": "Main_Diffusers_SD3_Config" + }, + "Main_Diffusers_SDXLRefiner_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "prediction_type": { + "$ref": "#/components/schemas/SchedulerPredictionType" + }, + "variant": { + "$ref": "#/components/schemas/ModelVariantType" + }, + "base": { + "type": "string", + "const": "sdxl-refiner", + "title": "Base", + "default": "sdxl-refiner" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "repo_variant", + "prediction_type", + "variant", + "base" + ], + "title": "Main_Diffusers_SDXLRefiner_Config" + }, + "Main_Diffusers_SDXL_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "prediction_type": { + "$ref": "#/components/schemas/SchedulerPredictionType" + }, + "variant": { + "$ref": "#/components/schemas/ModelVariantType" + }, + "base": { + "type": "string", + "const": "sdxl", + "title": "Base", + "default": "sdxl" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "repo_variant", + "prediction_type", + "variant", + "base" + ], + "title": "Main_Diffusers_SDXL_Config" + }, + "Main_Diffusers_ZImage_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "base": { + "type": "string", + "const": "z-image", + "title": "Base", + "default": "z-image" + }, + "variant": { + "$ref": "#/components/schemas/ZImageVariantType" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "format", + "repo_variant", + "base", + "variant" + ], + "title": "Main_Diffusers_ZImage_Config", + "description": "Model config for Z-Image diffusers models (Z-Image-Turbo, Z-Image-Base)." + }, + "Main_GGUF_FLUX_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "base": { + "type": "string", + "const": "flux", + "title": "Base", + "default": "flux" + }, + "format": { + "type": "string", + "const": "gguf_quantized", + "title": "Format", + "default": "gguf_quantized" + }, + "variant": { + "$ref": "#/components/schemas/FluxVariantType" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "config_path", + "base", + "format", + "variant" + ], + "title": "Main_GGUF_FLUX_Config", + "description": "Model config for main checkpoint models." + }, + "Main_GGUF_Flux2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "base": { + "type": "string", + "const": "flux2", + "title": "Base", + "default": "flux2" + }, + "format": { + "type": "string", + "const": "gguf_quantized", + "title": "Format", + "default": "gguf_quantized" + }, + "variant": { + "$ref": "#/components/schemas/Flux2VariantType" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "config_path", + "base", + "format", + "variant" + ], + "title": "Main_GGUF_Flux2_Config", + "description": "Model config for GGUF-quantized FLUX.2 checkpoint models (e.g. Klein)." + }, + "Main_GGUF_QwenImage_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "base": { + "type": "string", + "const": "qwen-image", + "title": "Base", + "default": "qwen-image" + }, + "format": { + "type": "string", + "const": "gguf_quantized", + "title": "Format", + "default": "gguf_quantized" + }, + "variant": { + "anyOf": [ + { + "$ref": "#/components/schemas/QwenImageVariantType" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "config_path", + "base", + "format", + "variant" + ], + "title": "Main_GGUF_QwenImage_Config", + "description": "Model config for GGUF-quantized Qwen Image transformer models." + }, + "Main_GGUF_ZImage_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "main", + "title": "Type", + "default": "main" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "type": "null" + } + ], + "description": "Default settings for this model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "base": { + "type": "string", + "const": "z-image", + "title": "Base", + "default": "z-image" + }, + "format": { + "type": "string", + "const": "gguf_quantized", + "title": "Format", + "default": "gguf_quantized" + }, + "variant": { + "$ref": "#/components/schemas/ZImageVariantType" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "trigger_phrases", + "default_settings", + "config_path", + "base", + "format", + "variant" + ], + "title": "Main_GGUF_ZImage_Config", + "description": "Model config for GGUF-quantized Z-Image transformer models." + }, + "MaskCombineInvocation": { + "category": "mask", + "class": "invocation", + "classification": "stable", + "description": "Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "mask1": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The first mask to combine", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "mask2": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The second image to combine", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "mask_combine", + "default": "mask_combine", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "mask", "multiply"], + "title": "Combine Masks", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "MaskEdgeInvocation": { + "category": "mask", + "class": "invocation", + "classification": "stable", + "description": "Applies an edge mask to an image", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to apply the mask to", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "edge_size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The size of the edge", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Edge Size" + }, + "edge_blur": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The amount of blur on the edge", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Edge Blur" + }, + "low_threshold": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "First threshold for the hysteresis procedure in Canny edge detection", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Low Threshold" + }, + "high_threshold": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Second threshold for the hysteresis procedure in Canny edge detection", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "High Threshold" + }, + "type": { + "const": "mask_edge", + "default": "mask_edge", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "mask", "inpaint"], + "title": "Mask Edge", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "MaskFromAlphaInvocation": { + "category": "mask", + "class": "invocation", + "classification": "stable", + "description": "Extracts the alpha channel of an image as a mask.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to create the mask from", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "invert": { + "default": false, + "description": "Whether or not to invert the mask", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Invert", + "type": "boolean" + }, + "type": { + "const": "tomask", + "default": "tomask", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "mask"], + "title": "Mask from Alpha", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "MaskFromIDInvocation": { + "category": "mask", + "class": "invocation", + "classification": "stable", + "description": "Generate a mask for a particular color in an ID Map", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to create the mask from", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "color": { + "anyOf": [ + { + "$ref": "#/components/schemas/ColorField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "ID color to mask", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "threshold": { + "default": 100, + "description": "Threshold for color detection", + "field_kind": "input", + "input": "any", + "orig_default": 100, + "orig_required": false, + "title": "Threshold", + "type": "integer" + }, + "invert": { + "default": false, + "description": "Whether or not to invert the mask", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Invert", + "type": "boolean" + }, + "type": { + "const": "mask_from_id", + "default": "mask_from_id", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "mask", "id"], + "title": "Mask from Segmented Image", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "MaskOutput": { + "class": "output", + "description": "A torch mask tensor.", + "properties": { + "mask": { + "$ref": "#/components/schemas/TensorField", + "description": "The mask.", + "field_kind": "output", + "ui_hidden": false + }, + "width": { + "description": "The width of the mask in pixels.", + "field_kind": "output", + "title": "Width", + "type": "integer", + "ui_hidden": false + }, + "height": { + "description": "The height of the mask in pixels.", + "field_kind": "output", + "title": "Height", + "type": "integer", + "ui_hidden": false + }, + "type": { + "const": "mask_output", + "default": "mask_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "mask", "width", "height", "type", "type"], + "title": "MaskOutput", + "type": "object" + }, + "MaskTensorToImageInvocation": { + "category": "mask", + "class": "invocation", + "classification": "stable", + "description": "Convert a mask tensor to an image.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The mask tensor to convert.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "tensor_mask_to_image", + "default": "tensor_mask_to_image", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["mask"], + "title": "Tensor Mask to Image", + "type": "object", + "version": "1.1.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "MediaPipeFaceDetectionInvocation": { + "category": "controlnet_preprocessors", + "class": "invocation", + "classification": "stable", + "description": "Detects faces using MediaPipe.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "max_faces": { + "default": 1, + "description": "Maximum number of faces to detect", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 1, + "orig_required": false, + "title": "Max Faces", + "type": "integer" + }, + "min_confidence": { + "default": 0.5, + "description": "Minimum confidence for face detection", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0.5, + "orig_required": false, + "title": "Min Confidence", + "type": "number" + }, + "type": { + "const": "mediapipe_face_detection", + "default": "mediapipe_face_detection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["controlnet", "face"], + "title": "MediaPipe Face Detection", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "MergeMetadataInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "stable", + "description": "Merged a collection of MetadataDict into a single MetadataDict.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "collection": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/MetadataField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Collection of Metadata", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Collection" + }, + "type": { + "const": "merge_metadata", + "default": "merge_metadata", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata Merge", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/MetadataOutput" + } + }, + "MergeTilesToImageInvocation": { + "category": "tiles", + "class": "invocation", + "classification": "stable", + "description": "Merge multiple tile images into a single image.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "tiles_with_images": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/TileWithImage" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A list of tile images with tile properties.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Tiles With Images" + }, + "blend_mode": { + "default": "Seam", + "description": "blending type Linear or Seam", + "enum": ["Linear", "Seam"], + "field_kind": "input", + "input": "direct", + "orig_default": "Seam", + "orig_required": false, + "title": "Blend Mode", + "type": "string" + }, + "blend_amount": { + "default": 32, + "description": "The amount to blend adjacent tiles in pixels. Must be <= the amount of overlap between adjacent tiles.", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 32, + "orig_required": false, + "title": "Blend Amount", + "type": "integer" + }, + "type": { + "const": "merge_tiles_to_image", + "default": "merge_tiles_to_image", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["tiles"], + "title": "Merge Tiles to Image", + "type": "object", + "version": "1.1.1", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "MetadataField": { + "additionalProperties": true, + "type": "object", + "title": "MetadataField", + "description": "Pydantic model for metadata with custom root of type dict[str, Any].\nMetadata is stored without a strict schema." + }, + "MetadataFieldExtractorInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "deprecated", + "description": "Extracts the text value from an image's metadata given a key.\nRaises an error if the image has no metadata or if the value is not a string (nesting not permitted).", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to extract metadata from", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The key in the image's metadata to extract the value from", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Key" + }, + "type": { + "const": "metadata_field_extractor", + "default": "metadata_field_extractor", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata Field Extractor", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/StringOutput" + } + }, + "MetadataFromImageInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Used to create a core metadata item then Add/Update it to the provided metadata", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "metadata_from_image", + "default": "metadata_from_image", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata From Image", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/MetadataOutput" + } + }, + "MetadataInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "stable", + "description": "Takes a MetadataItem or collection of MetadataItems and outputs a MetadataDict.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "items": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/MetadataItemField" + }, + "type": "array" + }, + { + "$ref": "#/components/schemas/MetadataItemField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A single metadata item or collection of metadata items", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Items" + }, + "type": { + "const": "metadata", + "default": "metadata", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/MetadataOutput" + } + }, + "MetadataItemField": { + "properties": { + "label": { + "description": "Label for this metadata item", + "title": "Label", + "type": "string" + }, + "value": { + "description": "The value for this metadata item (may be any type)", + "title": "Value" + } + }, + "required": ["label", "value"], + "title": "MetadataItemField", + "type": "object" + }, + "MetadataItemInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "stable", + "description": "Used to create an arbitrary metadata item. Provide \"label\" and make a connection to \"value\" to store that data as the value.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Label for this metadata item", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Label" + }, + "value": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "description": "The value for this metadata item (may be any type)", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Value", + "ui_type": "AnyField" + }, + "type": { + "const": "metadata_item", + "default": "metadata_item", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata Item", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/MetadataItemOutput" + } + }, + "MetadataItemLinkedInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Used to Create/Add/Update a value into a metadata label", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "label": { + "default": "* CUSTOM LABEL *", + "description": "Label for this metadata item", + "enum": [ + "* 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" + ], + "field_kind": "input", + "input": "direct", + "orig_default": "* CUSTOM LABEL *", + "orig_required": false, + "title": "Label", + "type": "string" + }, + "custom_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Label for this metadata item", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Custom Label" + }, + "value": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "description": "The value for this metadata item (may be any type)", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Value", + "ui_type": "AnyField" + }, + "type": { + "const": "metadata_item_linked", + "default": "metadata_item_linked", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata Item Linked", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/MetadataOutput" + } + }, + "MetadataItemOutput": { + "class": "output", + "description": "Metadata Item Output", + "properties": { + "item": { + "$ref": "#/components/schemas/MetadataItemField", + "description": "Metadata Item", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "metadata_item_output", + "default": "metadata_item_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "item", "type", "type"], + "title": "MetadataItemOutput", + "type": "object" + }, + "MetadataOutput": { + "class": "output", + "properties": { + "metadata": { + "$ref": "#/components/schemas/MetadataField", + "description": "Metadata Dict", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "metadata_output", + "default": "metadata_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "metadata", "type", "type"], + "title": "MetadataOutput", + "type": "object" + }, + "MetadataToBoolCollectionInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts a Boolean value Collection of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "label": { + "default": "* CUSTOM LABEL *", + "description": "Label for this metadata item", + "enum": ["* CUSTOM LABEL *", "seamless_x", "seamless_y"], + "field_kind": "input", + "input": "direct", + "orig_default": "* CUSTOM LABEL *", + "orig_required": false, + "title": "Label", + "type": "string" + }, + "custom_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Label for this metadata item", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Custom Label" + }, + "default_value": { + "anyOf": [ + { + "items": { + "type": "boolean" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The default bool to use if not found in the metadata", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Default Value" + }, + "type": { + "const": "metadata_to_bool_collection", + "default": "metadata_to_bool_collection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To Bool Collection", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/BooleanCollectionOutput" + } + }, + "MetadataToBoolInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts a Boolean value of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "label": { + "default": "* CUSTOM LABEL *", + "description": "Label for this metadata item", + "enum": ["* CUSTOM LABEL *", "seamless_x", "seamless_y"], + "field_kind": "input", + "input": "direct", + "orig_default": "* CUSTOM LABEL *", + "orig_required": false, + "title": "Label", + "type": "string" + }, + "custom_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Label for this metadata item", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Custom Label" + }, + "default_value": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The default bool to use if not found in the metadata", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Default Value" + }, + "type": { + "const": "metadata_to_bool", + "default": "metadata_to_bool", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To Bool", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/BooleanOutput" + } + }, + "MetadataToControlnetsInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts a Controlnets value of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "control_list": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlField" + }, + { + "items": { + "$ref": "#/components/schemas/ControlField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "ControlNet-List" + }, + "type": { + "const": "metadata_to_controlnets", + "default": "metadata_to_controlnets", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To ControlNets", + "type": "object", + "version": "1.2.0", + "output": { + "$ref": "#/components/schemas/MDControlListOutput" + } + }, + "MetadataToFloatCollectionInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts a Float value Collection of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "label": { + "default": "* CUSTOM LABEL *", + "description": "Label for this metadata item", + "enum": ["* CUSTOM LABEL *", "cfg_scale", "cfg_rescale_multiplier", "guidance"], + "field_kind": "input", + "input": "direct", + "orig_default": "* CUSTOM LABEL *", + "orig_required": false, + "title": "Label", + "type": "string" + }, + "custom_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Label for this metadata item", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Custom Label" + }, + "default_value": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The default float to use if not found in the metadata", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Default Value" + }, + "type": { + "const": "metadata_to_float_collection", + "default": "metadata_to_float_collection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To Float Collection", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/FloatCollectionOutput" + } + }, + "MetadataToFloatInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts a Float value of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "label": { + "default": "* CUSTOM LABEL *", + "description": "Label for this metadata item", + "enum": ["* CUSTOM LABEL *", "cfg_scale", "cfg_rescale_multiplier", "guidance"], + "field_kind": "input", + "input": "direct", + "orig_default": "* CUSTOM LABEL *", + "orig_required": false, + "title": "Label", + "type": "string" + }, + "custom_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Label for this metadata item", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Custom Label" + }, + "default_value": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The default float to use if not found in the metadata", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Default Value" + }, + "type": { + "const": "metadata_to_float", + "default": "metadata_to_float", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To Float", + "type": "object", + "version": "1.1.0", + "output": { + "$ref": "#/components/schemas/FloatOutput" + } + }, + "MetadataToIPAdaptersInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts a IP-Adapters value of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "ip_adapter_list": { + "anyOf": [ + { + "$ref": "#/components/schemas/IPAdapterField" + }, + { + "items": { + "$ref": "#/components/schemas/IPAdapterField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "IP-Adapter to apply", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "IP-Adapter-List" + }, + "type": { + "const": "metadata_to_ip_adapters", + "default": "metadata_to_ip_adapters", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To IP-Adapters", + "type": "object", + "version": "1.2.0", + "output": { + "$ref": "#/components/schemas/MDIPAdapterListOutput" + } + }, + "MetadataToIntegerCollectionInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts an integer value Collection of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "label": { + "default": "* CUSTOM LABEL *", + "description": "Label for this metadata item", + "enum": [ + "* CUSTOM LABEL *", + "width", + "height", + "seed", + "steps", + "clip_skip", + "cfg_scale_start_step", + "cfg_scale_end_step" + ], + "field_kind": "input", + "input": "direct", + "orig_default": "* CUSTOM LABEL *", + "orig_required": false, + "title": "Label", + "type": "string" + }, + "custom_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Label for this metadata item", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Custom Label" + }, + "default_value": { + "anyOf": [ + { + "items": { + "type": "integer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The default integer to use if not found in the metadata", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Default Value" + }, + "type": { + "const": "metadata_to_integer_collection", + "default": "metadata_to_integer_collection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To Integer Collection", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/IntegerCollectionOutput" + } + }, + "MetadataToIntegerInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts an integer value of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "label": { + "default": "* CUSTOM LABEL *", + "description": "Label for this metadata item", + "enum": [ + "* CUSTOM LABEL *", + "width", + "height", + "seed", + "steps", + "clip_skip", + "cfg_scale_start_step", + "cfg_scale_end_step" + ], + "field_kind": "input", + "input": "direct", + "orig_default": "* CUSTOM LABEL *", + "orig_required": false, + "title": "Label", + "type": "string" + }, + "custom_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Label for this metadata item", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Custom Label" + }, + "default_value": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The default integer to use if not found in the metadata", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Default Value" + }, + "type": { + "const": "metadata_to_integer", + "default": "metadata_to_integer", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To Integer", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/IntegerOutput" + } + }, + "MetadataToLorasCollectionInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts Lora(s) from metadata into a collection", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "custom_label": { + "default": "loras", + "description": "Label for this metadata item", + "field_kind": "input", + "input": "direct", + "orig_default": "loras", + "orig_required": false, + "title": "Custom Label", + "type": "string" + }, + "loras": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoRAField" + }, + { + "items": { + "$ref": "#/components/schemas/LoRAField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": [], + "description": "LoRA models and weights. May be a single LoRA or collection.", + "field_kind": "input", + "input": "any", + "orig_default": [], + "orig_required": false, + "title": "LoRAs" + }, + "type": { + "const": "metadata_to_lora_collection", + "default": "metadata_to_lora_collection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To LoRA Collection", + "type": "object", + "version": "1.1.0", + "output": { + "$ref": "#/components/schemas/MetadataToLorasCollectionOutput" + } + }, + "MetadataToLorasCollectionOutput": { + "class": "output", + "description": "Model loader output", + "properties": { + "lora": { + "description": "Collection of LoRA model and weights", + "field_kind": "output", + "items": { + "$ref": "#/components/schemas/LoRAField" + }, + "title": "LoRAs", + "type": "array", + "ui_hidden": false + }, + "type": { + "const": "metadata_to_lora_collection_output", + "default": "metadata_to_lora_collection_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "lora", "type", "type"], + "title": "MetadataToLorasCollectionOutput", + "type": "object" + }, + "MetadataToLorasInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts a Loras value of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "unet": { + "anyOf": [ + { + "$ref": "#/components/schemas/UNetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "UNet (scheduler, LoRAs)", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "UNet" + }, + "clip": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "CLIP" + }, + "type": { + "const": "metadata_to_loras", + "default": "metadata_to_loras", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To LoRAs", + "type": "object", + "version": "1.1.1", + "output": { + "$ref": "#/components/schemas/LoRALoaderOutput" + } + }, + "MetadataToModelInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts a Model value of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "label": { + "default": "model", + "description": "Label for this metadata item", + "enum": ["* CUSTOM LABEL *", "model"], + "field_kind": "input", + "input": "direct", + "orig_default": "model", + "orig_required": false, + "title": "Label", + "type": "string" + }, + "custom_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Label for this metadata item", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Custom Label" + }, + "default_value": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The default model to use if not found in the metadata", + "field_kind": "input", + "input": "any", + "orig_required": true, + "ui_model_type": ["main"] + }, + "type": { + "const": "metadata_to_model", + "default": "metadata_to_model", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To Model", + "type": "object", + "version": "1.3.0", + "output": { + "$ref": "#/components/schemas/MetadataToModelOutput" + } + }, + "MetadataToModelOutput": { + "class": "output", + "description": "String to main model output", + "properties": { + "model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Main model (UNet, VAE, CLIP) to load", + "field_kind": "output", + "title": "Model", + "ui_hidden": false + }, + "name": { + "description": "Model Name", + "field_kind": "output", + "title": "Name", + "type": "string", + "ui_hidden": false + }, + "unet": { + "$ref": "#/components/schemas/UNetField", + "description": "UNet (scheduler, LoRAs)", + "field_kind": "output", + "title": "UNet", + "ui_hidden": false + }, + "vae": { + "$ref": "#/components/schemas/VAEField", + "description": "VAE", + "field_kind": "output", + "title": "VAE", + "ui_hidden": false + }, + "clip": { + "$ref": "#/components/schemas/CLIPField", + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "output", + "title": "CLIP", + "ui_hidden": false + }, + "type": { + "const": "metadata_to_model_output", + "default": "metadata_to_model_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "model", "name", "unet", "vae", "clip", "type", "type"], + "title": "MetadataToModelOutput", + "type": "object" + }, + "MetadataToSDXLLorasInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts a SDXL Loras value of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "unet": { + "anyOf": [ + { + "$ref": "#/components/schemas/UNetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "UNet (scheduler, LoRAs)", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "UNet" + }, + "clip": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "CLIP 1" + }, + "clip2": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "CLIP 2" + }, + "type": { + "const": "metadata_to_sdlx_loras", + "default": "metadata_to_sdlx_loras", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To SDXL LoRAs", + "type": "object", + "version": "1.1.1", + "output": { + "$ref": "#/components/schemas/SDXLLoRALoaderOutput" + } + }, + "MetadataToSDXLModelInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts a SDXL Model value of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "label": { + "default": "model", + "description": "Label for this metadata item", + "enum": ["* CUSTOM LABEL *", "model"], + "field_kind": "input", + "input": "direct", + "orig_default": "model", + "orig_required": false, + "title": "Label", + "type": "string" + }, + "custom_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Label for this metadata item", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Custom Label" + }, + "default_value": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The default SDXL Model to use if not found in the metadata", + "field_kind": "input", + "input": "any", + "orig_required": true, + "ui_model_base": ["sdxl"], + "ui_model_type": ["main"] + }, + "type": { + "const": "metadata_to_sdxl_model", + "default": "metadata_to_sdxl_model", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To SDXL Model", + "type": "object", + "version": "1.3.0", + "output": { + "$ref": "#/components/schemas/MetadataToSDXLModelOutput" + } + }, + "MetadataToSDXLModelOutput": { + "class": "output", + "description": "String to SDXL main model output", + "properties": { + "model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Main model (UNet, VAE, CLIP) to load", + "field_kind": "output", + "title": "Model", + "ui_hidden": false + }, + "name": { + "description": "Model Name", + "field_kind": "output", + "title": "Name", + "type": "string", + "ui_hidden": false + }, + "unet": { + "$ref": "#/components/schemas/UNetField", + "description": "UNet (scheduler, LoRAs)", + "field_kind": "output", + "title": "UNet", + "ui_hidden": false + }, + "clip": { + "$ref": "#/components/schemas/CLIPField", + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "output", + "title": "CLIP 1", + "ui_hidden": false + }, + "clip2": { + "$ref": "#/components/schemas/CLIPField", + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "output", + "title": "CLIP 2", + "ui_hidden": false + }, + "vae": { + "$ref": "#/components/schemas/VAEField", + "description": "VAE", + "field_kind": "output", + "title": "VAE", + "ui_hidden": false + }, + "type": { + "const": "metadata_to_sdxl_model_output", + "default": "metadata_to_sdxl_model_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "model", "name", "unet", "clip", "clip2", "vae", "type", "type"], + "title": "MetadataToSDXLModelOutput", + "type": "object" + }, + "MetadataToSchedulerInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts a Scheduler value of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "label": { + "default": "scheduler", + "description": "Label for this metadata item", + "enum": ["* CUSTOM LABEL *", "scheduler"], + "field_kind": "input", + "input": "direct", + "orig_default": "scheduler", + "orig_required": false, + "title": "Label", + "type": "string" + }, + "custom_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Label for this metadata item", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Custom Label" + }, + "default_value": { + "default": "euler", + "description": "The default scheduler to use if not found in the metadata", + "enum": [ + "ddim", + "ddpm", + "deis", + "deis_k", + "lms", + "lms_k", + "pndm", + "heun", + "heun_k", + "euler", + "euler_k", + "euler_a", + "kdpm_2", + "kdpm_2_k", + "kdpm_2_a", + "kdpm_2_a_k", + "dpmpp_2s", + "dpmpp_2s_k", + "dpmpp_2m", + "dpmpp_2m_k", + "dpmpp_2m_sde", + "dpmpp_2m_sde_k", + "dpmpp_3m", + "dpmpp_3m_k", + "dpmpp_sde", + "dpmpp_sde_k", + "er_sde", + "unipc", + "unipc_k", + "lcm", + "tcd" + ], + "field_kind": "input", + "input": "any", + "orig_default": "euler", + "orig_required": false, + "title": "Default Value", + "type": "string", + "ui_type": "SchedulerField" + }, + "type": { + "const": "metadata_to_scheduler", + "default": "metadata_to_scheduler", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To Scheduler", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/SchedulerOutput" + } + }, + "MetadataToStringCollectionInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts a string collection value of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "label": { + "default": "* CUSTOM LABEL *", + "description": "Label for this metadata item", + "enum": [ + "* CUSTOM LABEL *", + "positive_prompt", + "positive_style_prompt", + "negative_prompt", + "negative_style_prompt" + ], + "field_kind": "input", + "input": "direct", + "orig_default": "* CUSTOM LABEL *", + "orig_required": false, + "title": "Label", + "type": "string" + }, + "custom_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Label for this metadata item", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Custom Label" + }, + "default_value": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The default string collection to use if not found in the metadata", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Default Value" + }, + "type": { + "const": "metadata_to_string_collection", + "default": "metadata_to_string_collection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To String Collection", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/StringCollectionOutput" + } + }, + "MetadataToStringInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts a string value of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "label": { + "default": "* CUSTOM LABEL *", + "description": "Label for this metadata item", + "enum": [ + "* CUSTOM LABEL *", + "positive_prompt", + "positive_style_prompt", + "negative_prompt", + "negative_style_prompt" + ], + "field_kind": "input", + "input": "direct", + "orig_default": "* CUSTOM LABEL *", + "orig_required": false, + "title": "Label", + "type": "string" + }, + "custom_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Label for this metadata item", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Custom Label" + }, + "default_value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The default string to use if not found in the metadata", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Default Value" + }, + "type": { + "const": "metadata_to_string", + "default": "metadata_to_string", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To String", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/StringOutput" + } + }, + "MetadataToT2IAdaptersInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts a T2I-Adapters value of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "t2i_adapter_list": { + "anyOf": [ + { + "$ref": "#/components/schemas/T2IAdapterField" + }, + { + "items": { + "$ref": "#/components/schemas/T2IAdapterField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "IP-Adapter to apply", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "T2I-Adapter" + }, + "type": { + "const": "metadata_to_t2i_adapters", + "default": "metadata_to_t2i_adapters", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To T2I-Adapters", + "type": "object", + "version": "1.2.0", + "output": { + "$ref": "#/components/schemas/MDT2IAdapterListOutput" + } + }, + "MetadataToVAEInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "beta", + "description": "Extracts a VAE value of a label from metadata", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "label": { + "default": "vae", + "description": "Label for this metadata item", + "enum": ["* CUSTOM LABEL *", "vae"], + "field_kind": "input", + "input": "direct", + "orig_default": "vae", + "orig_required": false, + "title": "Label", + "type": "string" + }, + "custom_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Label for this metadata item", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Custom Label" + }, + "default_value": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The default VAE to use if not found in the metadata", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "metadata_to_vae", + "default": "metadata_to_vae", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["metadata"], + "title": "Metadata To VAE", + "type": "object", + "version": "1.2.1", + "output": { + "$ref": "#/components/schemas/VAEOutput" + } + }, + "ModelFormat": { + "type": "string", + "enum": [ + "omi", + "diffusers", + "checkpoint", + "lycoris", + "onnx", + "olive", + "embedding_file", + "embedding_folder", + "invokeai", + "t5_encoder", + "qwen3_encoder", + "qwen_vl_encoder", + "bnb_quantized_int8b", + "bnb_quantized_nf4b", + "gguf_quantized", + "external_api", + "unknown" + ], + "title": "ModelFormat", + "description": "Storage format of model." + }, + "ModelIdentifierField": { + "properties": { + "key": { + "description": "The model's unique key", + "title": "Key", + "type": "string" + }, + "hash": { + "description": "The model's BLAKE3 hash", + "title": "Hash", + "type": "string" + }, + "name": { + "description": "The model's name", + "title": "Name", + "type": "string" + }, + "base": { + "$ref": "#/components/schemas/BaseModelType", + "description": "The model's base model type" + }, + "type": { + "$ref": "#/components/schemas/ModelType", + "description": "The model's type" + }, + "submodel_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/SubModelType" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The submodel to load, if this is a main model" + } + }, + "required": ["key", "hash", "name", "base", "type"], + "title": "ModelIdentifierField", + "type": "object" + }, + "ModelIdentifierInvocation": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Selects any model, outputting it its identifier. Be careful with this one! The identifier will be accepted as\ninput for any model, even if the model types don't match. If you connect this to a mismatched input, you'll get an\nerror.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The model to select", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Model" + }, + "type": { + "const": "model_identifier", + "default": "model_identifier", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["model"], + "title": "Any Model", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/ModelIdentifierOutput" + } + }, + "ModelIdentifierOutput": { + "class": "output", + "description": "Model identifier output", + "properties": { + "model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Model identifier", + "field_kind": "output", + "title": "Model", + "ui_hidden": false + }, + "type": { + "const": "model_identifier_output", + "default": "model_identifier_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "model", "type", "type"], + "title": "ModelIdentifierOutput", + "type": "object" + }, + "ModelInstallCancelledEvent": { + "description": "Event model for model_install_cancelled", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "id": { + "description": "The ID of the install job", + "title": "Id", + "type": "integer" + }, + "source": { + "description": "Source of the model; local path, repo_id or url", + "discriminator": { + "mapping": { + "external": "#/components/schemas/ExternalModelSource", + "hf": "#/components/schemas/HFModelSource", + "local": "#/components/schemas/LocalModelSource", + "url": "#/components/schemas/URLModelSource" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/LocalModelSource" + }, + { + "$ref": "#/components/schemas/HFModelSource" + }, + { + "$ref": "#/components/schemas/URLModelSource" + }, + { + "$ref": "#/components/schemas/ExternalModelSource" + } + ], + "title": "Source" + } + }, + "required": ["timestamp", "id", "source"], + "title": "ModelInstallCancelledEvent", + "type": "object" + }, + "ModelInstallCompleteEvent": { + "description": "Event model for model_install_complete", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "id": { + "description": "The ID of the install job", + "title": "Id", + "type": "integer" + }, + "source": { + "description": "Source of the model; local path, repo_id or url", + "discriminator": { + "mapping": { + "external": "#/components/schemas/ExternalModelSource", + "hf": "#/components/schemas/HFModelSource", + "local": "#/components/schemas/LocalModelSource", + "url": "#/components/schemas/URLModelSource" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/LocalModelSource" + }, + { + "$ref": "#/components/schemas/HFModelSource" + }, + { + "$ref": "#/components/schemas/URLModelSource" + }, + { + "$ref": "#/components/schemas/ExternalModelSource" + } + ], + "title": "Source" + }, + "key": { + "description": "Model config record key", + "title": "Key", + "type": "string" + }, + "total_bytes": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Size of the model (may be None for installation of a local path)", + "title": "Total Bytes" + }, + "config": { + "description": "The installed model's config", + "oneOf": [ + { + "$ref": "#/components/schemas/Main_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD3_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_ZImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SDXL_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Spandrel_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config" + }, + { + "$ref": "#/components/schemas/CLIPVision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/SigLIP_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/TextLLM_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/ExternalApiModelConfig" + }, + { + "$ref": "#/components/schemas/Unknown_Config" + } + ], + "title": "Config" + } + }, + "required": ["timestamp", "id", "source", "key", "total_bytes", "config"], + "title": "ModelInstallCompleteEvent", + "type": "object" + }, + "ModelInstallDownloadProgressEvent": { + "description": "Event model for model_install_download_progress", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "id": { + "description": "The ID of the install job", + "title": "Id", + "type": "integer" + }, + "source": { + "description": "Source of the model; local path, repo_id or url", + "discriminator": { + "mapping": { + "external": "#/components/schemas/ExternalModelSource", + "hf": "#/components/schemas/HFModelSource", + "local": "#/components/schemas/LocalModelSource", + "url": "#/components/schemas/URLModelSource" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/LocalModelSource" + }, + { + "$ref": "#/components/schemas/HFModelSource" + }, + { + "$ref": "#/components/schemas/URLModelSource" + }, + { + "$ref": "#/components/schemas/ExternalModelSource" + } + ], + "title": "Source" + }, + "local_path": { + "description": "Where model is downloading to", + "title": "Local Path", + "type": "string" + }, + "bytes": { + "description": "Number of bytes downloaded so far", + "title": "Bytes", + "type": "integer" + }, + "total_bytes": { + "description": "Total size of download, including all files", + "title": "Total Bytes", + "type": "integer" + }, + "parts": { + "description": "Progress of downloading URLs that comprise the model, if any", + "items": { + "additionalProperties": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + }, + "type": "object" + }, + "title": "Parts", + "type": "array" + } + }, + "required": ["timestamp", "id", "source", "local_path", "bytes", "total_bytes", "parts"], + "title": "ModelInstallDownloadProgressEvent", + "type": "object" + }, + "ModelInstallDownloadStartedEvent": { + "description": "Event model for model_install_download_started", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "id": { + "description": "The ID of the install job", + "title": "Id", + "type": "integer" + }, + "source": { + "description": "Source of the model; local path, repo_id or url", + "discriminator": { + "mapping": { + "external": "#/components/schemas/ExternalModelSource", + "hf": "#/components/schemas/HFModelSource", + "local": "#/components/schemas/LocalModelSource", + "url": "#/components/schemas/URLModelSource" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/LocalModelSource" + }, + { + "$ref": "#/components/schemas/HFModelSource" + }, + { + "$ref": "#/components/schemas/URLModelSource" + }, + { + "$ref": "#/components/schemas/ExternalModelSource" + } + ], + "title": "Source" + }, + "local_path": { + "description": "Where model is downloading to", + "title": "Local Path", + "type": "string" + }, + "bytes": { + "description": "Number of bytes downloaded so far", + "title": "Bytes", + "type": "integer" + }, + "total_bytes": { + "description": "Total size of download, including all files", + "title": "Total Bytes", + "type": "integer" + }, + "parts": { + "description": "Progress of downloading URLs that comprise the model, if any", + "items": { + "additionalProperties": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + }, + "type": "object" + }, + "title": "Parts", + "type": "array" + } + }, + "required": ["timestamp", "id", "source", "local_path", "bytes", "total_bytes", "parts"], + "title": "ModelInstallDownloadStartedEvent", + "type": "object" + }, + "ModelInstallDownloadsCompleteEvent": { + "description": "Emitted once when an install job becomes active.", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "id": { + "description": "The ID of the install job", + "title": "Id", + "type": "integer" + }, + "source": { + "description": "Source of the model; local path, repo_id or url", + "discriminator": { + "mapping": { + "external": "#/components/schemas/ExternalModelSource", + "hf": "#/components/schemas/HFModelSource", + "local": "#/components/schemas/LocalModelSource", + "url": "#/components/schemas/URLModelSource" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/LocalModelSource" + }, + { + "$ref": "#/components/schemas/HFModelSource" + }, + { + "$ref": "#/components/schemas/URLModelSource" + }, + { + "$ref": "#/components/schemas/ExternalModelSource" + } + ], + "title": "Source" + } + }, + "required": ["timestamp", "id", "source"], + "title": "ModelInstallDownloadsCompleteEvent", + "type": "object" + }, + "ModelInstallErrorEvent": { + "description": "Event model for model_install_error", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "id": { + "description": "The ID of the install job", + "title": "Id", + "type": "integer" + }, + "source": { + "description": "Source of the model; local path, repo_id or url", + "discriminator": { + "mapping": { + "external": "#/components/schemas/ExternalModelSource", + "hf": "#/components/schemas/HFModelSource", + "local": "#/components/schemas/LocalModelSource", + "url": "#/components/schemas/URLModelSource" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/LocalModelSource" + }, + { + "$ref": "#/components/schemas/HFModelSource" + }, + { + "$ref": "#/components/schemas/URLModelSource" + }, + { + "$ref": "#/components/schemas/ExternalModelSource" + } + ], + "title": "Source" + }, + "error_type": { + "description": "The name of the exception", + "title": "Error Type", + "type": "string" + }, + "error": { + "description": "A text description of the exception", + "title": "Error", + "type": "string" + } + }, + "required": ["timestamp", "id", "source", "error_type", "error"], + "title": "ModelInstallErrorEvent", + "type": "object" + }, + "ModelInstallJob": { + "properties": { + "id": { + "type": "integer", + "title": "Id", + "description": "Unique ID for this job" + }, + "status": { + "$ref": "#/components/schemas/InstallStatus", + "description": "Current status of install process", + "default": "waiting" + }, + "error_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Reason", + "description": "Information about why the job failed" + }, + "config_in": { + "$ref": "#/components/schemas/ModelRecordChanges", + "description": "Configuration information (e.g. 'description') to apply to model." + }, + "config_out": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/Main_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD3_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_ZImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SDXL_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Spandrel_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config" + }, + { + "$ref": "#/components/schemas/CLIPVision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/SigLIP_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/TextLLM_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/ExternalApiModelConfig" + }, + { + "$ref": "#/components/schemas/Unknown_Config" + } + ] + }, + { + "type": "null" + } + ], + "title": "Config Out", + "description": "After successful installation, this will hold the configuration object." + }, + "inplace": { + "type": "boolean", + "title": "Inplace", + "description": "Leave model in its current location; otherwise install under models directory", + "default": false + }, + "source": { + "oneOf": [ + { + "$ref": "#/components/schemas/LocalModelSource" + }, + { + "$ref": "#/components/schemas/HFModelSource" + }, + { + "$ref": "#/components/schemas/URLModelSource" + }, + { + "$ref": "#/components/schemas/ExternalModelSource" + } + ], + "title": "Source", + "description": "Source (URL, repo_id, or local path) of model", + "discriminator": { + "propertyName": "type", + "mapping": { + "external": "#/components/schemas/ExternalModelSource", + "hf": "#/components/schemas/HFModelSource", + "local": "#/components/schemas/LocalModelSource", + "url": "#/components/schemas/URLModelSource" + } + } + }, + "local_path": { + "type": "string", + "format": "path", + "title": "Local Path", + "description": "Path to locally-downloaded model; may be the same as the source" + }, + "bytes": { + "type": "integer", + "title": "Bytes", + "description": "For a remote model, the number of bytes downloaded so far (may not be available)", + "default": 0 + }, + "total_bytes": { + "type": "integer", + "title": "Total Bytes", + "description": "Total size of the model to be installed", + "default": 0 + }, + "source_metadata": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BaseMetadata" + }, + { + "$ref": "#/components/schemas/HuggingFaceMetadata" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "basemetadata": "#/components/schemas/BaseMetadata", + "huggingface": "#/components/schemas/HuggingFaceMetadata" + } + } + }, + { + "type": "null" + } + ], + "title": "Source Metadata", + "description": "Metadata provided by the model source" + }, + "download_parts": { + "items": { + "$ref": "#/components/schemas/DownloadJob" + }, + "type": "array", + "uniqueItems": true, + "title": "Download Parts", + "description": "Download jobs contributing to this install" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error", + "description": "On an error condition, this field will contain the text of the exception" + }, + "error_traceback": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Traceback", + "description": "On an error condition, this field will contain the exception traceback" + } + }, + "type": "object", + "required": ["id", "source", "local_path"], + "title": "ModelInstallJob", + "description": "Object that tracks the current status of an install request." + }, + "ModelInstallStartedEvent": { + "description": "Event model for model_install_started", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "id": { + "description": "The ID of the install job", + "title": "Id", + "type": "integer" + }, + "source": { + "description": "Source of the model; local path, repo_id or url", + "discriminator": { + "mapping": { + "external": "#/components/schemas/ExternalModelSource", + "hf": "#/components/schemas/HFModelSource", + "local": "#/components/schemas/LocalModelSource", + "url": "#/components/schemas/URLModelSource" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/LocalModelSource" + }, + { + "$ref": "#/components/schemas/HFModelSource" + }, + { + "$ref": "#/components/schemas/URLModelSource" + }, + { + "$ref": "#/components/schemas/ExternalModelSource" + } + ], + "title": "Source" + } + }, + "required": ["timestamp", "id", "source"], + "title": "ModelInstallStartedEvent", + "type": "object" + }, + "ModelLoadCompleteEvent": { + "description": "Event model for model_load_complete", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "config": { + "description": "The model's config", + "oneOf": [ + { + "$ref": "#/components/schemas/Main_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD3_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_ZImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SDXL_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Spandrel_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config" + }, + { + "$ref": "#/components/schemas/CLIPVision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/SigLIP_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/TextLLM_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/ExternalApiModelConfig" + }, + { + "$ref": "#/components/schemas/Unknown_Config" + } + ], + "title": "Config" + }, + "submodel_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/SubModelType" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The submodel type, if any" + } + }, + "required": ["timestamp", "config", "submodel_type"], + "title": "ModelLoadCompleteEvent", + "type": "object" + }, + "ModelLoadStartedEvent": { + "description": "Event model for model_load_started", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "config": { + "description": "The model's config", + "oneOf": [ + { + "$ref": "#/components/schemas/Main_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD3_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_ZImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SDXL_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Spandrel_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config" + }, + { + "$ref": "#/components/schemas/CLIPVision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/SigLIP_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/TextLLM_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/ExternalApiModelConfig" + }, + { + "$ref": "#/components/schemas/Unknown_Config" + } + ], + "title": "Config" + }, + "submodel_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/SubModelType" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The submodel type, if any" + } + }, + "required": ["timestamp", "config", "submodel_type"], + "title": "ModelLoadStartedEvent", + "type": "object" + }, + "ModelLoaderOutput": { + "class": "output", + "description": "Model loader output", + "properties": { + "vae": { + "$ref": "#/components/schemas/VAEField", + "description": "VAE", + "field_kind": "output", + "title": "VAE", + "ui_hidden": false + }, + "type": { + "const": "model_loader_output", + "default": "model_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + }, + "clip": { + "$ref": "#/components/schemas/CLIPField", + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "output", + "title": "CLIP", + "ui_hidden": false + }, + "unet": { + "$ref": "#/components/schemas/UNetField", + "description": "UNet (scheduler, LoRAs)", + "field_kind": "output", + "title": "UNet", + "ui_hidden": false + } + }, + "required": ["output_meta", "vae", "type", "clip", "unet", "type"], + "title": "ModelLoaderOutput", + "type": "object" + }, + "ModelRecordChanges": { + "properties": { + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source", + "description": "original source of the model" + }, + "source_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelSourceType" + }, + { + "type": "null" + } + ], + "description": "type of model source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "metadata from remote source" + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page)" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "Name of the model." + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Path", + "description": "Path to the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "base": { + "anyOf": [ + { + "$ref": "#/components/schemas/BaseModelType" + }, + { + "type": "null" + } + ], + "description": "The base model." + }, + "type": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelType" + }, + { + "type": "null" + } + ], + "description": "Type of model" + }, + "key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Key", + "description": "Database ID for this model" + }, + "hash": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Hash", + "description": "hash of model file" + }, + "file_size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "File Size", + "description": "Size of model file" + }, + "format": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Format", + "description": "format of model file" + }, + "trigger_phrases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + { + "type": "null" + } + ], + "title": "Trigger Phrases", + "description": "Set of trigger phrases for this model" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/MainModelDefaultSettings" + }, + { + "$ref": "#/components/schemas/LoraModelDefaultSettings" + }, + { + "$ref": "#/components/schemas/ControlAdapterDefaultSettings" + }, + { + "$ref": "#/components/schemas/ExternalApiModelDefaultSettings" + }, + { + "type": "null" + } + ], + "title": "Default Settings", + "description": "Default settings for this model" + }, + "provider_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider Id", + "description": "External provider identifier" + }, + "provider_model_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider Model Id", + "description": "External provider model identifier" + }, + "capabilities": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExternalModelCapabilities" + }, + { + "type": "null" + } + ], + "description": "External model capabilities" + }, + "cpu_only": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Cpu Only", + "description": "Whether this model should run on CPU only" + }, + "variant": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelVariantType" + }, + { + "$ref": "#/components/schemas/ClipVariantType" + }, + { + "$ref": "#/components/schemas/FluxVariantType" + }, + { + "$ref": "#/components/schemas/Flux2VariantType" + }, + { + "$ref": "#/components/schemas/ZImageVariantType" + }, + { + "$ref": "#/components/schemas/QwenImageVariantType" + }, + { + "$ref": "#/components/schemas/Qwen3VariantType" + }, + { + "type": "null" + } + ], + "title": "Variant", + "description": "The variant of the model." + }, + "prediction_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/SchedulerPredictionType" + }, + { + "type": "null" + } + ], + "description": "The prediction type of the model." + }, + "upcast_attention": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Upcast Attention", + "description": "Whether to upcast attention." + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to config file for model" + } + }, + "type": "object", + "title": "ModelRecordChanges", + "description": "A set of changes to apply to a model." + }, + "ModelRecordOrderBy": { + "type": "string", + "enum": ["default", "type", "base", "name", "format", "size", "created_at", "updated_at", "path"], + "title": "ModelRecordOrderBy", + "description": "The order in which to return model summaries." + }, + "ModelRelationshipBatchRequest": { + "properties": { + "model_keys": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Model Keys", + "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"] + ] + } + }, + "type": "object", + "required": ["model_keys"], + "title": "ModelRelationshipBatchRequest" + }, + "ModelRelationshipCreateRequest": { + "properties": { + "model_key_1": { + "type": "string", + "title": "Model Key 1", + "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": { + "type": "string", + "title": "Model Key 2", + "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" + ] + } + }, + "type": "object", + "required": ["model_key_1", "model_key_2"], + "title": "ModelRelationshipCreateRequest" + }, + "ModelRepoVariant": { + "type": "string", + "enum": ["", "fp16", "fp32", "onnx", "openvino", "flax"], + "title": "ModelRepoVariant", + "description": "Various hugging face variants on the diffusers format." + }, + "ModelSourceType": { + "type": "string", + "enum": ["path", "url", "hf_repo_id", "external"], + "title": "ModelSourceType", + "description": "Model source type." + }, + "ModelType": { + "type": "string", + "enum": [ + "onnx", + "main", + "vae", + "lora", + "control_lora", + "controlnet", + "embedding", + "ip_adapter", + "clip_vision", + "clip_embed", + "t2i_adapter", + "t5_encoder", + "qwen3_encoder", + "qwen_vl_encoder", + "spandrel_image_to_image", + "siglip", + "flux_redux", + "llava_onevision", + "text_llm", + "external_image_generator", + "unknown" + ], + "title": "ModelType", + "description": "Model type." + }, + "ModelVariantType": { + "type": "string", + "enum": ["normal", "inpaint", "depth"], + "title": "ModelVariantType", + "description": "Variant type." + }, + "ModelsList": { + "properties": { + "models": { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/Main_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_SD3_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_CogView4_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_SDXLRefiner_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/Main_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/Main_BnBNF4_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_Flux2_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_FLUX_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/Main_GGUF_ZImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Flux2_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/VAE_Checkpoint_Anima_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/VAE_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Checkpoint_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/ControlNet_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_ZImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_QwenImage_Config" + }, + { + "$ref": "#/components/schemas/LoRA_LyCORIS_Anima_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_OMI_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SD2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_Flux2_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_FLUX_Config" + }, + { + "$ref": "#/components/schemas/LoRA_Diffusers_ZImage_Config" + }, + { + "$ref": "#/components/schemas/ControlLoRA_LyCORIS_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_T5Encoder_Config" + }, + { + "$ref": "#/components/schemas/T5Encoder_BnBLLMint8_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Qwen3Encoder_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/Qwen3Encoder_GGUF_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/QwenVLEncoder_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_File_SDXL_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD1_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SD2_Config" + }, + { + "$ref": "#/components/schemas/TI_Folder_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_InvokeAI_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD1_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SD2_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_SDXL_Config" + }, + { + "$ref": "#/components/schemas/IPAdapter_Checkpoint_FLUX_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SD1_Config" + }, + { + "$ref": "#/components/schemas/T2IAdapter_Diffusers_SDXL_Config" + }, + { + "$ref": "#/components/schemas/Spandrel_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_G_Config" + }, + { + "$ref": "#/components/schemas/CLIPEmbed_Diffusers_L_Config" + }, + { + "$ref": "#/components/schemas/CLIPVision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/SigLIP_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/FLUXRedux_Checkpoint_Config" + }, + { + "$ref": "#/components/schemas/LlavaOnevision_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/TextLLM_Diffusers_Config" + }, + { + "$ref": "#/components/schemas/ExternalApiModelConfig" + }, + { + "$ref": "#/components/schemas/Unknown_Config" + } + ] + }, + "type": "array", + "title": "Models" + } + }, + "type": "object", + "required": ["models"], + "title": "ModelsList", + "description": "Return list of configs." + }, + "MultiplyInvocation": { + "category": "math", + "class": "invocation", + "classification": "stable", + "description": "Multiplies two numbers", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "a": { + "default": 0, + "description": "The first number", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "A", + "type": "integer" + }, + "b": { + "default": 0, + "description": "The second number", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "B", + "type": "integer" + }, + "type": { + "const": "mul", + "default": "mul", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["math", "multiply"], + "title": "Multiply Integers", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/IntegerOutput" + } + }, + "NodeFieldValue": { + "properties": { + "node_path": { + "type": "string", + "title": "Node Path", + "description": "The node into which this batch data item will be substituted." + }, + "field_name": { + "type": "string", + "title": "Field Name", + "description": "The field into which this batch data item will be substituted." + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "integer" + }, + { + "$ref": "#/components/schemas/ImageField" + } + ], + "title": "Value", + "description": "The value to substitute into the node/field." + } + }, + "type": "object", + "required": ["node_path", "field_name", "value"], + "title": "NodeFieldValue" + }, + "NodePackInfo": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the node pack." + }, + "path": { + "type": "string", + "title": "Path", + "description": "The path to the node pack directory." + }, + "node_count": { + "type": "integer", + "title": "Node Count", + "description": "The number of nodes in the pack." + }, + "node_types": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Node Types", + "description": "The invocation types provided by this node pack." + } + }, + "type": "object", + "required": ["name", "path", "node_count", "node_types"], + "title": "NodePackInfo", + "description": "Information about an installed node pack." + }, + "NodePackListResponse": { + "properties": { + "node_packs": { + "items": { + "$ref": "#/components/schemas/NodePackInfo" + }, + "type": "array", + "title": "Node Packs", + "description": "List of installed node packs." + }, + "custom_nodes_path": { + "type": "string", + "title": "Custom Nodes Path", + "description": "The configured custom nodes directory path." + } + }, + "type": "object", + "required": ["node_packs", "custom_nodes_path"], + "title": "NodePackListResponse", + "description": "Response for listing installed node packs." + }, + "NoiseInvocation": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Generates latent noise for supported denoiser architectures.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "noise_type": { + "default": "SD", + "description": "Architecture-specific noise type.", + "enum": ["SD", "FLUX", "FLUX.2", "SD3", "CogView4", "Z-Image", "Anima"], + "field_kind": "input", + "input": "any", + "orig_default": "SD", + "orig_required": false, + "title": "Noise Type", + "type": "string" + }, + "seed": { + "default": 0, + "description": "Seed for random number generation", + "field_kind": "input", + "input": "any", + "maximum": 4294967295, + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Seed", + "type": "integer" + }, + "width": { + "default": 512, + "description": "Width of output (px)", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "multipleOf": 8, + "orig_default": 512, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 512, + "description": "Height of output (px)", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "multipleOf": 8, + "orig_default": 512, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "use_cpu": { + "default": true, + "description": "Use CPU for noise generation (for reproducible results across platforms)", + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "Use Cpu", + "type": "boolean" + }, + "type": { + "const": "noise", + "default": "noise", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "noise"], + "title": "Create Latent Noise", + "type": "object", + "version": "1.1.0", + "output": { + "$ref": "#/components/schemas/NoiseOutput" + } + }, + "NoiseOutput": { + "class": "output", + "description": "Invocation noise output", + "properties": { + "noise": { + "$ref": "#/components/schemas/LatentsField", + "description": "Noise tensor", + "field_kind": "output", + "ui_hidden": false + }, + "width": { + "description": "Width of output (px)", + "field_kind": "output", + "title": "Width", + "type": "integer", + "ui_hidden": false + }, + "height": { + "description": "Height of output (px)", + "field_kind": "output", + "title": "Height", + "type": "integer", + "ui_hidden": false + }, + "type": { + "const": "noise_output", + "default": "noise_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "noise", "width", "height", "type", "type"], + "title": "NoiseOutput", + "type": "object" + }, + "NormalMapInvocation": { + "category": "controlnet_preprocessors", + "class": "invocation", + "classification": "stable", + "description": "Generates a normal map.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "normal_map", + "default": "normal_map", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["controlnet", "normal"], + "title": "Normal Map", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "OffsetPaginatedResults_BoardDTO_": { + "properties": { + "limit": { + "type": "integer", + "title": "Limit", + "description": "Limit of items to get" + }, + "offset": { + "type": "integer", + "title": "Offset", + "description": "Offset from which to retrieve items" + }, + "total": { + "type": "integer", + "title": "Total", + "description": "Total number of items in result" + }, + "items": { + "items": { + "$ref": "#/components/schemas/BoardDTO" + }, + "type": "array", + "title": "Items", + "description": "Items" + } + }, + "type": "object", + "required": ["limit", "offset", "total", "items"], + "title": "OffsetPaginatedResults[BoardDTO]" + }, + "OffsetPaginatedResults_ImageDTO_": { + "properties": { + "limit": { + "type": "integer", + "title": "Limit", + "description": "Limit of items to get" + }, + "offset": { + "type": "integer", + "title": "Offset", + "description": "Offset from which to retrieve items" + }, + "total": { + "type": "integer", + "title": "Total", + "description": "Total number of items in result" + }, + "items": { + "items": { + "$ref": "#/components/schemas/ImageDTO" + }, + "type": "array", + "title": "Items", + "description": "Items" + } + }, + "type": "object", + "required": ["limit", "offset", "total", "items"], + "title": "OffsetPaginatedResults[ImageDTO]" + }, + "OklabUnsharpMaskInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Applies an unsharp mask filter to an image in the Oklab color space", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to use", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "radius": { + "default": 2, + "description": "Unsharp mask radius", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 2, + "orig_required": false, + "title": "Radius", + "type": "number" + }, + "strength": { + "default": 50, + "description": "Unsharp mask strength", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 50, + "orig_required": false, + "title": "Strength", + "type": "number" + }, + "type": { + "const": "unsharp_mask_oklab", + "default": "unsharp_mask_oklab", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "unsharp_mask", "oklab"], + "title": "Unsharp Mask (Oklab)", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "OklchImageHueAdjustmentInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Adjusts the hue of an image in Oklch space.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to adjust", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "hue": { + "default": 0, + "description": "The degrees by which to rotate the hue, 0-360", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Hue", + "type": "integer" + }, + "type": { + "const": "img_hue_adjust_oklch", + "default": "img_hue_adjust_oklch", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "hue", "oklch"], + "title": "Adjust Image Hue (Oklch)", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "OpenAIImageGenerationInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Generate images using an OpenAI-hosted external model.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Main model (UNet, VAE, CLIP) to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "ui_model_base": ["external"], + "ui_model_format": ["external_api"], + "ui_model_provider_id": ["openai"], + "ui_model_type": ["external_image_generator"] + }, + "mode": { + "default": "txt2img", + "description": "Generation mode.", + "enum": ["txt2img", "img2img", "inpaint"], + "field_kind": "input", + "input": "any", + "orig_default": "txt2img", + "orig_required": false, + "title": "Mode", + "type": "string", + "ui_hidden": true + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Prompt", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Prompt" + }, + "seed": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Seed for random number generation", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Seed" + }, + "num_images": { + "default": 1, + "description": "Number of images to generate", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 1, + "orig_required": false, + "title": "Num Images", + "type": "integer" + }, + "width": { + "default": 1024, + "description": "Width of output (px)", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 1024, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 1024, + "description": "Height of output (px)", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 1024, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "image_size": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Image size preset (e.g. 1K, 2K, 4K)", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Image Size" + }, + "init_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Init image (use reference_images instead)", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "ui_hidden": true + }, + "mask_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Mask image for inpaint", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "ui_hidden": true + }, + "reference_images": { + "default": [], + "description": "Reference images", + "field_kind": "input", + "input": "any", + "items": { + "$ref": "#/components/schemas/ImageField" + }, + "orig_default": [], + "orig_required": false, + "title": "Reference Images", + "type": "array" + }, + "quality": { + "default": "auto", + "description": "Output image quality", + "enum": ["auto", "high", "medium", "low"], + "field_kind": "input", + "input": "any", + "orig_default": "auto", + "orig_required": false, + "title": "Quality", + "type": "string" + }, + "background": { + "default": "auto", + "description": "Background transparency handling", + "enum": ["auto", "transparent", "opaque"], + "field_kind": "input", + "input": "any", + "orig_default": "auto", + "orig_required": false, + "title": "Background", + "type": "string" + }, + "input_fidelity": { + "anyOf": [ + { + "enum": ["low", "high"], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Fidelity to source images (edits only)", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Input Fidelity" + }, + "type": { + "const": "openai_image_generation", + "default": "openai_image_generation", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["external", "generation", "openai"], + "title": "OpenAI Image Generation", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageCollectionOutput" + } + }, + "OrphanedModelInfo": { + "properties": { + "path": { + "type": "string", + "title": "Path", + "description": "Relative path to the orphaned directory from models root" + }, + "absolute_path": { + "type": "string", + "title": "Absolute Path", + "description": "Absolute path to the orphaned directory" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Files", + "description": "List of model files in this directory" + }, + "size_bytes": { + "type": "integer", + "title": "Size Bytes", + "description": "Total size of all files in bytes" + } + }, + "type": "object", + "required": ["path", "absolute_path", "files", "size_bytes"], + "title": "OrphanedModelInfo", + "description": "Information about an orphaned model directory." + }, + "OutputFieldJSONSchemaExtra": { + "description": "Extra attributes to be added to input fields and their OpenAPI schema. Used by the workflow editor\nduring schema parsing and UI rendering.", + "properties": { + "field_kind": { + "$ref": "#/components/schemas/FieldKind" + }, + "ui_hidden": { + "default": false, + "title": "Ui Hidden", + "type": "boolean" + }, + "ui_order": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ui Order" + }, + "ui_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/UIType" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": ["field_kind", "ui_hidden", "ui_order", "ui_type"], + "title": "OutputFieldJSONSchemaExtra", + "type": "object" + }, + "PBRMapsInvocation": { + "category": "controlnet_preprocessors", + "class": "invocation", + "classification": "stable", + "description": "Generate Normal, Displacement and Roughness Map from a given image", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Input image", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "tile_size": { + "default": 512, + "description": "Tile size", + "field_kind": "input", + "input": "any", + "orig_default": 512, + "orig_required": false, + "title": "Tile Size", + "type": "integer" + }, + "border_mode": { + "default": "none", + "description": "Border mode to apply to eliminate any artifacts or seams", + "enum": ["none", "seamless", "mirror", "replicate"], + "field_kind": "input", + "input": "any", + "orig_default": "none", + "orig_required": false, + "title": "Border Mode", + "type": "string" + }, + "type": { + "const": "pbr_maps", + "default": "pbr_maps", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "material"], + "title": "PBR Maps", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/PBRMapsOutput" + } + }, + "PBRMapsOutput": { + "class": "output", + "properties": { + "normal_map": { + "$ref": "#/components/schemas/ImageField", + "default": null, + "description": "The generated normal map", + "field_kind": "output", + "ui_hidden": false + }, + "roughness_map": { + "$ref": "#/components/schemas/ImageField", + "default": null, + "description": "The generated roughness map", + "field_kind": "output", + "ui_hidden": false + }, + "displacement_map": { + "$ref": "#/components/schemas/ImageField", + "default": null, + "description": "The generated displacement map", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "pbr_maps-output", + "default": "pbr_maps-output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "normal_map", "roughness_map", "displacement_map", "type", "type"], + "title": "PBRMapsOutput", + "type": "object" + }, + "PaginatedResults_WorkflowRecordListItemWithThumbnailDTO_": { + "properties": { + "page": { + "type": "integer", + "title": "Page", + "description": "Current Page" + }, + "pages": { + "type": "integer", + "title": "Pages", + "description": "Total number of pages" + }, + "per_page": { + "type": "integer", + "title": "Per Page", + "description": "Number of items per page" + }, + "total": { + "type": "integer", + "title": "Total", + "description": "Total number of items in result" + }, + "items": { + "items": { + "$ref": "#/components/schemas/WorkflowRecordListItemWithThumbnailDTO" + }, + "type": "array", + "title": "Items", + "description": "Items" + } + }, + "type": "object", + "required": ["page", "pages", "per_page", "total", "items"], + "title": "PaginatedResults[WorkflowRecordListItemWithThumbnailDTO]" + }, + "PairTileImageInvocation": { + "category": "tiles", + "class": "invocation", + "classification": "stable", + "description": "Pair an image with its tile properties.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The tile image.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "tile": { + "anyOf": [ + { + "$ref": "#/components/schemas/Tile" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The tile properties.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "pair_tile_image", + "default": "pair_tile_image", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["tiles"], + "title": "Pair Tile with Image", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/PairTileImageOutput" + } + }, + "PairTileImageOutput": { + "class": "output", + "properties": { + "tile_with_image": { + "$ref": "#/components/schemas/TileWithImage", + "description": "A tile description with its corresponding image.", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "pair_tile_image_output", + "default": "pair_tile_image_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "tile_with_image", "type", "type"], + "title": "PairTileImageOutput", + "type": "object" + }, + "PasteImageIntoBoundingBoxInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Paste the source image into the target image at the given bounding box.\n\nThe source image must be the same size as the bounding box, and the bounding box must fit within the target image.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "source_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to paste", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "target_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to paste into", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "bounding_box": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoundingBoxField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The bounding box to paste the image into", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "paste_image_into_bounding_box", + "default": "paste_image_into_bounding_box", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "crop"], + "title": "Paste Image into Bounding Box", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "PiDiNetEdgeDetectionInvocation": { + "category": "controlnet_preprocessors", + "class": "invocation", + "classification": "stable", + "description": "Generates an edge map using PiDiNet.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "quantize_edges": { + "default": false, + "description": "Whether or not to use safe mode", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Quantize Edges", + "type": "boolean" + }, + "scribble": { + "default": false, + "description": "Whether or not to use scribble mode", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Scribble", + "type": "boolean" + }, + "type": { + "const": "pidi_edge_detection", + "default": "pidi_edge_detection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["controlnet", "edge"], + "title": "PiDiNet Edge Detection", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "PresetData": { + "properties": { + "positive_prompt": { + "type": "string", + "title": "Positive Prompt", + "description": "Positive prompt" + }, + "negative_prompt": { + "type": "string", + "title": "Negative Prompt", + "description": "Negative prompt" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["positive_prompt", "negative_prompt"], + "title": "PresetData" + }, + "PresetType": { + "type": "string", + "enum": ["user", "default"], + "title": "PresetType" + }, + "ProgressImage": { + "description": "The progress image sent intermittently during processing", + "properties": { + "width": { + "description": "The effective width of the image in pixels", + "minimum": 1, + "title": "Width", + "type": "integer" + }, + "height": { + "description": "The effective height of the image in pixels", + "minimum": 1, + "title": "Height", + "type": "integer" + }, + "dataURL": { + "description": "The image data as a b64 data URL", + "title": "Dataurl", + "type": "string" + } + }, + "required": ["width", "height", "dataURL"], + "title": "ProgressImage", + "type": "object" + }, + "PromptTemplateInvocation": { + "category": "prompt", + "class": "invocation", + "classification": "stable", + "description": "Applies a Style Preset template to positive and negative prompts.\n\nSelect a Style Preset and provide positive/negative prompts. The node replaces\n{prompt} placeholders in the template with your input prompts.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "style_preset": { + "anyOf": [ + { + "$ref": "#/components/schemas/StylePresetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The Style Preset to use as a template", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "positive_prompt": { + "default": "", + "description": "The positive prompt to insert into the template's {prompt} placeholder", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Positive Prompt", + "type": "string", + "ui_component": "textarea" + }, + "negative_prompt": { + "default": "", + "description": "The negative prompt to insert into the template's {prompt} placeholder", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Negative Prompt", + "type": "string", + "ui_component": "textarea" + }, + "type": { + "const": "prompt_template", + "default": "prompt_template", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["prompt", "template", "style", "preset"], + "title": "Prompt Template", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/PromptTemplateOutput" + } + }, + "PromptTemplateOutput": { + "class": "output", + "description": "Output for the Prompt Template node", + "properties": { + "positive_prompt": { + "description": "The positive prompt with the template applied", + "field_kind": "output", + "title": "Positive Prompt", + "type": "string", + "ui_hidden": false + }, + "negative_prompt": { + "description": "The negative prompt with the template applied", + "field_kind": "output", + "title": "Negative Prompt", + "type": "string", + "ui_hidden": false + }, + "type": { + "const": "prompt_template_output", + "default": "prompt_template_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "positive_prompt", "negative_prompt", "type", "type"], + "title": "PromptTemplateOutput", + "type": "object" + }, + "PromptsFromFileInvocation": { + "category": "prompt", + "class": "invocation", + "classification": "stable", + "description": "Loads prompts from a text file", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "file_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Path to prompt text file", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "File Path" + }, + "pre_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "String to prepend to each prompt", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Pre Prompt", + "ui_component": "textarea" + }, + "post_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "String to append to each prompt", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Post Prompt", + "ui_component": "textarea" + }, + "start_line": { + "default": 1, + "description": "Line in the file to start start from", + "field_kind": "input", + "input": "any", + "minimum": 1, + "orig_default": 1, + "orig_required": false, + "title": "Start Line", + "type": "integer" + }, + "max_prompts": { + "default": 1, + "description": "Max lines to read from file (0=all)", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 1, + "orig_required": false, + "title": "Max Prompts", + "type": "integer" + }, + "type": { + "const": "prompt_from_file", + "default": "prompt_from_file", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["prompt", "file"], + "title": "Prompts from File", + "type": "object", + "version": "1.0.2", + "output": { + "$ref": "#/components/schemas/StringCollectionOutput" + } + }, + "PruneResult": { + "properties": { + "deleted": { + "type": "integer", + "title": "Deleted", + "description": "Number of queue items deleted" + } + }, + "type": "object", + "required": ["deleted"], + "title": "PruneResult", + "description": "Result of pruning the session queue" + }, + "QueueClearedEvent": { + "description": "Event model for queue_cleared", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "queue_id": { + "description": "The ID of the queue", + "title": "Queue Id", + "type": "string" + } + }, + "required": ["timestamp", "queue_id"], + "title": "QueueClearedEvent", + "type": "object" + }, + "QueueItemStatusChangedEvent": { + "description": "Event model for queue_item_status_changed", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "queue_id": { + "description": "The ID of the queue", + "title": "Queue Id", + "type": "string" + }, + "item_id": { + "description": "The ID of the queue item", + "title": "Item Id", + "type": "integer" + }, + "batch_id": { + "description": "The ID of the queue batch", + "title": "Batch Id", + "type": "string" + }, + "origin": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The origin of the queue item", + "title": "Origin" + }, + "destination": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The destination of the queue item", + "title": "Destination" + }, + "user_id": { + "default": "system", + "description": "The ID of the user who created the queue item", + "title": "User Id", + "type": "string" + }, + "status": { + "description": "The new status of the queue item", + "enum": ["pending", "in_progress", "completed", "failed", "canceled"], + "title": "Status", + "type": "string" + }, + "status_sequence": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A monotonically increasing version for this queue item's visible status lifecycle", + "title": "Status Sequence" + }, + "error_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The error type, if any", + "title": "Error Type" + }, + "error_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The error message, if any", + "title": "Error Message" + }, + "error_traceback": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The error traceback, if any", + "title": "Error Traceback" + }, + "created_at": { + "description": "The timestamp when the queue item was created", + "title": "Created At", + "type": "string" + }, + "updated_at": { + "description": "The timestamp when the queue item was last updated", + "title": "Updated At", + "type": "string" + }, + "started_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The timestamp when the queue item was started", + "title": "Started At" + }, + "completed_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The timestamp when the queue item was completed", + "title": "Completed At" + }, + "batch_status": { + "$ref": "#/components/schemas/BatchStatus", + "description": "The status of the batch" + }, + "queue_status": { + "$ref": "#/components/schemas/SessionQueueStatus", + "description": "The status of the queue" + }, + "session_id": { + "description": "The ID of the session (aka graph execution state)", + "title": "Session Id", + "type": "string" + } + }, + "required": [ + "timestamp", + "queue_id", + "item_id", + "batch_id", + "origin", + "destination", + "user_id", + "status", + "status_sequence", + "error_type", + "error_message", + "error_traceback", + "created_at", + "updated_at", + "started_at", + "completed_at", + "batch_status", + "queue_status", + "session_id" + ], + "title": "QueueItemStatusChangedEvent", + "type": "object" + }, + "QueueItemsRetriedEvent": { + "description": "Event model for queue_items_retried", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "queue_id": { + "description": "The ID of the queue", + "title": "Queue Id", + "type": "string" + }, + "retried_item_ids": { + "description": "The IDs of the queue items that were retried", + "items": { + "type": "integer" + }, + "title": "Retried Item Ids", + "type": "array" + } + }, + "required": ["timestamp", "queue_id", "retried_item_ids"], + "title": "QueueItemsRetriedEvent", + "type": "object" + }, + "Qwen3EncoderField": { + "description": "Field for Qwen3 text encoder used by Z-Image models.", + "properties": { + "tokenizer": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Info to load tokenizer submodel" + }, + "text_encoder": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Info to load text_encoder submodel" + }, + "loras": { + "description": "LoRAs to apply on model loading", + "items": { + "$ref": "#/components/schemas/LoRAField" + }, + "title": "Loras", + "type": "array" + } + }, + "required": ["tokenizer", "text_encoder"], + "title": "Qwen3EncoderField", + "type": "object" + }, + "Qwen3Encoder_Checkpoint_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "base": { + "type": "string", + "const": "any", + "title": "Base", + "default": "any" + }, + "type": { + "type": "string", + "const": "qwen3_encoder", + "title": "Type", + "default": "qwen3_encoder" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "cpu_only": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Cpu Only", + "description": "Whether this model should run on CPU only" + }, + "variant": { + "$ref": "#/components/schemas/Qwen3VariantType", + "description": "Qwen3 model size variant (4B or 8B)" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "config_path", + "base", + "type", + "format", + "cpu_only", + "variant" + ], + "title": "Qwen3Encoder_Checkpoint_Config", + "description": "Configuration for single-file Qwen3 Encoder models (safetensors)." + }, + "Qwen3Encoder_GGUF_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "base": { + "type": "string", + "const": "any", + "title": "Base", + "default": "any" + }, + "type": { + "type": "string", + "const": "qwen3_encoder", + "title": "Type", + "default": "qwen3_encoder" + }, + "format": { + "type": "string", + "const": "gguf_quantized", + "title": "Format", + "default": "gguf_quantized" + }, + "cpu_only": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Cpu Only", + "description": "Whether this model should run on CPU only" + }, + "variant": { + "$ref": "#/components/schemas/Qwen3VariantType", + "description": "Qwen3 model size variant (4B or 8B)" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "config_path", + "base", + "type", + "format", + "cpu_only", + "variant" + ], + "title": "Qwen3Encoder_GGUF_Config", + "description": "Configuration for GGUF-quantized Qwen3 Encoder models." + }, + "Qwen3Encoder_Qwen3Encoder_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "base": { + "type": "string", + "const": "any", + "title": "Base", + "default": "any" + }, + "type": { + "type": "string", + "const": "qwen3_encoder", + "title": "Type", + "default": "qwen3_encoder" + }, + "format": { + "type": "string", + "const": "qwen3_encoder", + "title": "Format", + "default": "qwen3_encoder" + }, + "cpu_only": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Cpu Only", + "description": "Whether this model should run on CPU only" + }, + "variant": { + "$ref": "#/components/schemas/Qwen3VariantType", + "description": "Qwen3 model size variant (4B or 8B)" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "base", + "type", + "format", + "cpu_only", + "variant" + ], + "title": "Qwen3Encoder_Qwen3Encoder_Config", + "description": "Configuration for Qwen3 Encoder models in a diffusers-like format.\n\nThe model weights are expected to be in a folder called text_encoder inside the model directory,\ncompatible with Qwen2VLForConditionalGeneration or similar architectures used by Z-Image." + }, + "Qwen3VariantType": { + "type": "string", + "enum": ["qwen3_4b", "qwen3_8b", "qwen3_06b"], + "title": "Qwen3VariantType", + "description": "Qwen3 text encoder variants based on model size." + }, + "QwenImageConditioningField": { + "description": "A Qwen Image Edit conditioning tensor primitive value", + "properties": { + "conditioning_name": { + "description": "The name of conditioning tensor", + "title": "Conditioning Name", + "type": "string" + } + }, + "required": ["conditioning_name"], + "title": "QwenImageConditioningField", + "type": "object" + }, + "QwenImageConditioningOutput": { + "class": "output", + "description": "Base class for nodes that output a Qwen Image Edit conditioning tensor.", + "properties": { + "conditioning": { + "$ref": "#/components/schemas/QwenImageConditioningField", + "description": "Conditioning tensor", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "qwen_image_conditioning_output", + "default": "qwen_image_conditioning_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "conditioning", "type", "type"], + "title": "QwenImageConditioningOutput", + "type": "object" + }, + "QwenImageDenoiseInvocation": { + "category": "image", + "class": "invocation", + "classification": "prototype", + "description": "Run the denoising process with a Qwen Image model.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "reference_latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Reference image latents to guide generation. Encoded through the VAE.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoise_mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/DenoiseMaskField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "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.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoising_start": { + "default": 0.0, + "description": "When to start denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0.0, + "orig_required": false, + "title": "Denoising Start", + "type": "number" + }, + "denoising_end": { + "default": 1.0, + "description": "When to stop denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Denoising End", + "type": "number" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Qwen Image Edit model (Transformer) to load", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Transformer" + }, + "positive_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/QwenImageConditioningField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Positive conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "negative_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/QwenImageConditioningField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Negative conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "cfg_scale": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 4.0, + "description": "Classifier-Free Guidance scale", + "field_kind": "input", + "input": "any", + "orig_default": 4.0, + "orig_required": false, + "title": "CFG Scale" + }, + "width": { + "default": 1024, + "description": "Width of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 16, + "orig_default": 1024, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 1024, + "description": "Height of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 16, + "orig_default": 1024, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "steps": { + "default": 40, + "description": "Number of steps to run", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 40, + "orig_required": false, + "title": "Steps", + "type": "integer" + }, + "seed": { + "default": 0, + "description": "Randomness seed for reproducibility.", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Seed", + "type": "integer" + }, + "shift": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "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.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Shift" + }, + "type": { + "const": "qwen_image_denoise", + "default": "qwen_image_denoise", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "qwen_image"], + "title": "Denoise - Qwen Image", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "QwenImageImageToLatentsInvocation": { + "category": "image", + "class": "invocation", + "classification": "prototype", + "description": "Generates latents from an image using the Qwen Image VAE.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to encode.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "width": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Resize the image to this width before encoding. If not set, encodes at the image's original size.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Width" + }, + "height": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Resize the image to this height before encoding. If not set, encodes at the image's original size.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Height" + }, + "type": { + "const": "qwen_image_i2l", + "default": "qwen_image_i2l", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "latents", "vae", "i2l", "qwen_image"], + "title": "Image to Latents - Qwen Image", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "QwenImageLatentsToImageInvocation": { + "category": "latents", + "class": "invocation", + "classification": "prototype", + "description": "Generates an image from latents using the Qwen Image VAE.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "type": { + "const": "qwen_image_l2i", + "default": "qwen_image_l2i", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "image", "vae", "l2i", "qwen_image"], + "title": "Latents to Image - Qwen Image", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "QwenImageLoRACollectionLoader": { + "category": "model", + "class": "invocation", + "classification": "prototype", + "description": "Applies a collection of LoRAs to a Qwen Image transformer.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "loras": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoRAField" + }, + { + "items": { + "$ref": "#/components/schemas/LoRAField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LoRA models and weights. May be a single LoRA or collection.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "LoRAs" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Transformer", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Transformer" + }, + "type": { + "const": "qwen_image_lora_collection_loader", + "default": "qwen_image_lora_collection_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["lora", "model", "qwen_image"], + "title": "Apply LoRA Collection - Qwen Image", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/QwenImageLoRALoaderOutput" + } + }, + "QwenImageLoRALoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "prototype", + "description": "Apply a LoRA model to a Qwen Image transformer.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "lora": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LoRA model to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "LoRA", + "ui_model_base": ["qwen-image"], + "ui_model_type": ["lora"] + }, + "weight": { + "default": 1.0, + "description": "The weight at which the LoRA is applied to each model", + "field_kind": "input", + "input": "any", + "orig_default": 1.0, + "orig_required": false, + "title": "Weight", + "type": "number" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Transformer", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Transformer" + }, + "type": { + "const": "qwen_image_lora_loader", + "default": "qwen_image_lora_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["lora", "model", "qwen_image"], + "title": "Apply LoRA - Qwen Image", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/QwenImageLoRALoaderOutput" + } + }, + "QwenImageLoRALoaderOutput": { + "class": "output", + "description": "Qwen Image LoRA Loader Output", + "properties": { + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Transformer", + "field_kind": "output", + "title": "Transformer", + "ui_hidden": false + }, + "type": { + "const": "qwen_image_lora_loader_output", + "default": "qwen_image_lora_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "transformer", "type", "type"], + "title": "QwenImageLoRALoaderOutput", + "type": "object" + }, + "QwenImageModelLoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "prototype", + "description": "Loads a Qwen Image model, outputting its submodels.\n\nThe transformer is always loaded from the main model (Diffusers or GGUF).\n\nComponents can be mixed and matched:\n- VAE: standalone Qwen Image VAE checkpoint, the Component Source (Diffusers),\n or the main model if it's Diffusers.\n- Qwen VL Encoder: standalone Qwen2.5-VL encoder, the Component Source\n (Diffusers), or the main model if it's Diffusers.\n\nTogether, the standalone VAE and standalone encoder allow running a GGUF\ntransformer without ever downloading the full ~40 GB Diffusers pipeline.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Qwen Image Edit model (Transformer) to load", + "field_kind": "input", + "input": "direct", + "orig_required": true, + "title": "Transformer", + "ui_model_base": ["qwen-image"], + "ui_model_type": ["main"] + }, + "vae_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "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).", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "VAE", + "ui_model_base": ["qwen-image"], + "ui_model_type": ["vae"] + }, + "qwen_vl_encoder_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "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).", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Qwen VL Encoder", + "ui_model_type": ["qwen_vl_encoder"] + }, + "component_source": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "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.", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Component Source (Diffusers)", + "ui_model_base": ["qwen-image"], + "ui_model_format": ["diffusers"], + "ui_model_type": ["main"] + }, + "type": { + "const": "qwen_image_model_loader", + "default": "qwen_image_model_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["model", "type", "id"], + "tags": ["model", "qwen_image"], + "title": "Main Model - Qwen Image", + "type": "object", + "version": "1.2.0", + "output": { + "$ref": "#/components/schemas/QwenImageModelLoaderOutput" + } + }, + "QwenImageModelLoaderOutput": { + "class": "output", + "description": "Qwen Image model loader output.", + "properties": { + "transformer": { + "$ref": "#/components/schemas/TransformerField", + "description": "Transformer", + "field_kind": "output", + "title": "Transformer", + "ui_hidden": false + }, + "qwen_vl_encoder": { + "$ref": "#/components/schemas/QwenVLEncoderField", + "description": "Qwen2.5-VL tokenizer, processor and text/vision encoder", + "field_kind": "output", + "title": "Qwen VL Encoder", + "ui_hidden": false + }, + "vae": { + "$ref": "#/components/schemas/VAEField", + "description": "VAE", + "field_kind": "output", + "title": "VAE", + "ui_hidden": false + }, + "type": { + "const": "qwen_image_model_loader_output", + "default": "qwen_image_model_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "transformer", "qwen_vl_encoder", "vae", "type", "type"], + "title": "QwenImageModelLoaderOutput", + "type": "object" + }, + "QwenImageTextEncoderInvocation": { + "category": "conditioning", + "class": "invocation", + "classification": "prototype", + "description": "Encodes text and reference images for Qwen Image using Qwen2.5-VL.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Text prompt describing the desired edit.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Prompt", + "ui_component": "textarea" + }, + "reference_images": { + "default": [], + "description": "Reference images to guide the edit. The model can use multiple reference images.", + "field_kind": "input", + "input": "any", + "items": { + "$ref": "#/components/schemas/ImageField" + }, + "orig_default": [], + "orig_required": false, + "title": "Reference Images", + "type": "array" + }, + "qwen_vl_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/QwenVLEncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Qwen2.5-VL tokenizer, processor and text/vision encoder", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Qwen VL Encoder" + }, + "quantization": { + "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.", + "enum": ["none", "int8", "nf4"], + "field_kind": "input", + "input": "any", + "orig_default": "none", + "orig_required": false, + "title": "Quantization", + "type": "string" + }, + "type": { + "const": "qwen_image_text_encoder", + "default": "qwen_image_text_encoder", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["prompt", "conditioning", "qwen_image"], + "title": "Prompt - Qwen Image", + "type": "object", + "version": "1.2.0", + "output": { + "$ref": "#/components/schemas/QwenImageConditioningOutput" + } + }, + "QwenImageVariantType": { + "type": "string", + "enum": ["generate", "edit"], + "title": "QwenImageVariantType", + "description": "Qwen Image model variants." + }, + "QwenVLEncoderField": { + "description": "Field for Qwen2.5-VL encoder used by Qwen Image Edit models.", + "properties": { + "tokenizer": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Info to load tokenizer submodel" + }, + "text_encoder": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Info to load text_encoder submodel" + } + }, + "required": ["tokenizer", "text_encoder"], + "title": "QwenVLEncoderField", + "type": "object" + }, + "QwenVLEncoder_Checkpoint_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "base": { + "type": "string", + "const": "any", + "title": "Base", + "default": "any" + }, + "type": { + "type": "string", + "const": "qwen_vl_encoder", + "title": "Type", + "default": "qwen_vl_encoder" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "config_path", + "base", + "type", + "format" + ], + "title": "QwenVLEncoder_Checkpoint_Config", + "description": "Configuration for single-file Qwen2.5-VL encoder checkpoints (safetensors).\n\nThis matches ComfyUI-style consolidated single-file encoders such as\n`qwen_2.5_vl_7b_fp8_scaled.safetensors`, which bundle the language model\nand the visual tower into one file (typically with FP8 + per-tensor\n`weight_scale` ComfyUI quantization).\n\nThe matching tokenizer + processor are pulled from HuggingFace\n(`Qwen/Qwen2.5-VL-7B-Instruct`) on first use and cached for offline use." + }, + "QwenVLEncoder_Diffusers_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "base": { + "type": "string", + "const": "any", + "title": "Base", + "default": "any" + }, + "type": { + "type": "string", + "const": "qwen_vl_encoder", + "title": "Type", + "default": "qwen_vl_encoder" + }, + "format": { + "type": "string", + "const": "qwen_vl_encoder", + "title": "Format", + "default": "qwen_vl_encoder" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "base", + "type", + "format" + ], + "title": "QwenVLEncoder_Diffusers_Config", + "description": "Configuration for standalone Qwen2.5-VL encoder models in diffusers-style folder layout.\n\nExpected structure:\n /\n text_encoder/\n config.json (with `_class_name` or `architectures` listing\n `Qwen2_5_VLForConditionalGeneration`)\n model.safetensors\n tokenizer/\n tokenizer_config.json\n ...\n processor/ (optional, for vision preprocessing)\n preprocessor_config.json\n\nThis lets users avoid downloading the full ~40 GB Qwen Image diffusers pipeline\nwhen they only need the Qwen2.5-VL encoder for use with a GGUF transformer." + }, + "RandomFloatInvocation": { + "category": "math", + "class": "invocation", + "classification": "stable", + "description": "Outputs a single random float", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": false, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "low": { + "default": 0.0, + "description": "The inclusive low value", + "field_kind": "input", + "input": "any", + "orig_default": 0.0, + "orig_required": false, + "title": "Low", + "type": "number" + }, + "high": { + "default": 1.0, + "description": "The exclusive high value", + "field_kind": "input", + "input": "any", + "orig_default": 1.0, + "orig_required": false, + "title": "High", + "type": "number" + }, + "decimals": { + "default": 2, + "description": "The number of decimal places to round to", + "field_kind": "input", + "input": "any", + "orig_default": 2, + "orig_required": false, + "title": "Decimals", + "type": "integer" + }, + "type": { + "const": "rand_float", + "default": "rand_float", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["math", "float", "random"], + "title": "Random Float", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/FloatOutput" + } + }, + "RandomIntInvocation": { + "category": "math", + "class": "invocation", + "classification": "stable", + "description": "Outputs a single random integer.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": false, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "low": { + "default": 0, + "description": "The inclusive low value", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Low", + "type": "integer" + }, + "high": { + "default": 2147483647, + "description": "The exclusive high value", + "field_kind": "input", + "input": "any", + "orig_default": 2147483647, + "orig_required": false, + "title": "High", + "type": "integer" + }, + "type": { + "const": "rand_int", + "default": "rand_int", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["math", "random"], + "title": "Random Integer", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/IntegerOutput" + } + }, + "RandomRangeInvocation": { + "category": "batch", + "class": "invocation", + "classification": "stable", + "description": "Creates a collection of random numbers", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": false, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "low": { + "default": 0, + "description": "The inclusive low value", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Low", + "type": "integer" + }, + "high": { + "default": 2147483647, + "description": "The exclusive high value", + "field_kind": "input", + "input": "any", + "orig_default": 2147483647, + "orig_required": false, + "title": "High", + "type": "integer" + }, + "size": { + "default": 1, + "description": "The number of values to generate", + "field_kind": "input", + "input": "any", + "orig_default": 1, + "orig_required": false, + "title": "Size", + "type": "integer" + }, + "seed": { + "default": 0, + "description": "The seed for the RNG (omit for random)", + "field_kind": "input", + "input": "any", + "maximum": 4294967295, + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Seed", + "type": "integer" + }, + "type": { + "const": "random_range", + "default": "random_range", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["range", "integer", "random", "collection"], + "title": "Random Range", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/IntegerCollectionOutput" + } + }, + "RangeInvocation": { + "category": "batch", + "class": "invocation", + "classification": "stable", + "description": "Creates a range of numbers from start to stop with step", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "start": { + "default": 0, + "description": "The start of the range", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Start", + "type": "integer" + }, + "stop": { + "default": 10, + "description": "The stop of the range", + "field_kind": "input", + "input": "any", + "orig_default": 10, + "orig_required": false, + "title": "Stop", + "type": "integer" + }, + "step": { + "default": 1, + "description": "The step of the range", + "field_kind": "input", + "input": "any", + "orig_default": 1, + "orig_required": false, + "title": "Step", + "type": "integer" + }, + "type": { + "const": "range", + "default": "range", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["collection", "integer", "range"], + "title": "Integer Range", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/IntegerCollectionOutput" + } + }, + "RangeOfSizeInvocation": { + "category": "batch", + "class": "invocation", + "classification": "stable", + "description": "Creates a range from start to start + (size * step) incremented by step", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "start": { + "default": 0, + "description": "The start of the range", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Start", + "type": "integer" + }, + "size": { + "default": 1, + "description": "The number of values", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 1, + "orig_required": false, + "title": "Size", + "type": "integer" + }, + "step": { + "default": 1, + "description": "The step of the range", + "field_kind": "input", + "input": "any", + "orig_default": 1, + "orig_required": false, + "title": "Step", + "type": "integer" + }, + "type": { + "const": "range_of_size", + "default": "range_of_size", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["collection", "integer", "size", "range"], + "title": "Integer Range of Size", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/IntegerCollectionOutput" + } + }, + "RecallParameter": { + "properties": { + "positive_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Positive Prompt", + "description": "Positive prompt text" + }, + "negative_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Negative Prompt", + "description": "Negative prompt text" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model", + "description": "Main model name/identifier" + }, + "refiner_model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Refiner Model", + "description": "Refiner model name/identifier" + }, + "vae_model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Vae Model", + "description": "VAE model name/identifier" + }, + "scheduler": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Scheduler", + "description": "Scheduler name" + }, + "steps": { + "anyOf": [ + { + "type": "integer", + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Steps", + "description": "Number of generation steps" + }, + "refiner_steps": { + "anyOf": [ + { + "type": "integer", + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Refiner Steps", + "description": "Number of refiner steps" + }, + "cfg_scale": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cfg Scale", + "description": "CFG scale for guidance" + }, + "cfg_rescale_multiplier": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cfg Rescale Multiplier", + "description": "CFG rescale multiplier" + }, + "refiner_cfg_scale": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Refiner Cfg Scale", + "description": "Refiner CFG scale" + }, + "guidance": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Guidance", + "description": "Guidance scale" + }, + "width": { + "anyOf": [ + { + "type": "integer", + "minimum": 64.0 + }, + { + "type": "null" + } + ], + "title": "Width", + "description": "Image width in pixels" + }, + "height": { + "anyOf": [ + { + "type": "integer", + "minimum": 64.0 + }, + { + "type": "null" + } + ], + "title": "Height", + "description": "Image height in pixels" + }, + "seed": { + "anyOf": [ + { + "type": "integer", + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Seed", + "description": "Random seed" + }, + "denoise_strength": { + "anyOf": [ + { + "type": "number", + "maximum": 1.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Denoise Strength", + "description": "Denoising strength" + }, + "refiner_denoise_start": { + "anyOf": [ + { + "type": "number", + "maximum": 1.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Refiner Denoise Start", + "description": "Refiner denoising start" + }, + "clip_skip": { + "anyOf": [ + { + "type": "integer", + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Clip Skip", + "description": "CLIP skip layers" + }, + "seamless_x": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Seamless X", + "description": "Enable seamless X tiling" + }, + "seamless_y": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Seamless Y", + "description": "Enable seamless Y tiling" + }, + "refiner_positive_aesthetic_score": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Refiner Positive Aesthetic Score", + "description": "Refiner positive aesthetic score" + }, + "refiner_negative_aesthetic_score": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Refiner Negative Aesthetic Score", + "description": "Refiner negative aesthetic score" + }, + "loras": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/LoRARecallParameter" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Loras", + "description": "List of LoRAs with their weights" + }, + "control_layers": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ControlNetRecallParameter" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Control Layers", + "description": "List of control adapters (ControlNet, T2I Adapter, Control LoRA) with their settings" + }, + "ip_adapters": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/IPAdapterRecallParameter" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Ip Adapters", + "description": "List of IP Adapters with their settings" + }, + "reference_images": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ReferenceImageRecallParameter" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Reference Images", + "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." + } + }, + "additionalProperties": false, + "type": "object", + "title": "RecallParameter", + "description": "Request model for updating recallable parameters." + }, + "RecallParametersUpdatedEvent": { + "description": "Event model for recall_parameters_updated", + "properties": { + "timestamp": { + "description": "The timestamp of the event", + "title": "Timestamp", + "type": "integer" + }, + "queue_id": { + "description": "The ID of the queue", + "title": "Queue Id", + "type": "string" + }, + "user_id": { + "description": "The ID of the user whose recall parameters were updated", + "title": "User Id", + "type": "string" + }, + "parameters": { + "additionalProperties": true, + "description": "The recall parameters that were updated", + "title": "Parameters", + "type": "object" + } + }, + "required": ["timestamp", "queue_id", "user_id", "parameters"], + "title": "RecallParametersUpdatedEvent", + "type": "object" + }, + "RectangleMaskInvocation": { + "category": "mask", + "class": "invocation", + "classification": "stable", + "description": "Create a rectangular mask.", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "width": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The width of the entire mask.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Width" + }, + "height": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The height of the entire mask.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Height" + }, + "x_left": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The left x-coordinate of the rectangular masked region (inclusive).", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "X Left" + }, + "y_top": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The top y-coordinate of the rectangular masked region (inclusive).", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Y Top" + }, + "rectangle_width": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The width of the rectangular masked region.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Rectangle Width" + }, + "rectangle_height": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The height of the rectangular masked region.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Rectangle Height" + }, + "type": { + "const": "rectangle_mask", + "default": "rectangle_mask", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["conditioning"], + "title": "Create Rectangle Mask", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/MaskOutput" + } + }, + "ReferenceImageRecallParameter": { + "properties": { + "image_name": { + "type": "string", + "title": "Image Name", + "description": "The filename of the reference image in outputs/images" + } + }, + "type": "object", + "required": ["image_name"], + "title": "ReferenceImageRecallParameter", + "description": "Global reference-image configuration for recall.\n\nUsed for reference images that feed directly into the main model rather\nthan through a separate IP-Adapter / ControlNet model \u2014 for example\nFLUX.2 Klein, FLUX Kontext, and Qwen Image Edit. The receiving frontend\npicks the correct config type (``flux2_reference_image`` /\n``qwen_image_reference_image`` / ``flux_kontext_reference_image``) based\non the currently-selected main model." + }, + "RemoteModelFile": { + "properties": { + "url": { + "type": "string", + "minLength": 1, + "format": "uri", + "title": "Url", + "description": "The url to download this model file" + }, + "path": { + "type": "string", + "format": "path", + "title": "Path", + "description": "The path to the file, relative to the model root" + }, + "size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Size", + "description": "The size of this file, in bytes", + "default": 0 + }, + "sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sha256", + "description": "SHA256 hash of this model (not always available)" + } + }, + "type": "object", + "required": ["url", "path"], + "title": "RemoteModelFile", + "description": "Information about a downloadable file that forms part of a model." + }, + "RemoveImagesFromBoardResult": { + "properties": { + "affected_boards": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Affected Boards", + "description": "The ids of boards affected by the delete operation" + }, + "removed_images": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Removed Images", + "description": "The image names that were removed from their board" + } + }, + "type": "object", + "required": ["affected_boards", "removed_images"], + "title": "RemoveImagesFromBoardResult" + }, + "ResizeLatentsInvocation": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Resizes latents to explicit width/height (in pixels). Provided dimensions are floor-divided by 8.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "width": { + "anyOf": [ + { + "minimum": 64, + "multipleOf": 8, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Width of output (px)", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Width" + }, + "height": { + "anyOf": [ + { + "minimum": 64, + "multipleOf": 8, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Width of output (px)", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Height" + }, + "mode": { + "default": "bilinear", + "description": "Interpolation mode", + "enum": ["nearest", "linear", "bilinear", "bicubic", "trilinear", "area", "nearest-exact"], + "field_kind": "input", + "input": "any", + "orig_default": "bilinear", + "orig_required": false, + "title": "Mode", + "type": "string" + }, + "antialias": { + "default": false, + "description": "Whether or not to apply antialiasing (bilinear or bicubic only)", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Antialias", + "type": "boolean" + }, + "type": { + "const": "lresize", + "default": "lresize", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "resize"], + "title": "Resize Latents", + "type": "object", + "version": "1.0.2", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "ResourceOrigin": { + "type": "string", + "enum": ["internal", "external"], + "title": "ResourceOrigin", + "description": "The origin of a resource (eg image).\n\n- INTERNAL: The resource was created by the application.\n- EXTERNAL: The resource was not created by the application.\nThis may be a user-initiated upload, or an internal application upload (eg Canvas init image)." + }, + "RetryItemsResult": { + "properties": { + "queue_id": { + "type": "string", + "title": "Queue Id", + "description": "The ID of the queue" + }, + "retried_item_ids": { + "items": { + "type": "integer" + }, + "type": "array", + "title": "Retried Item Ids", + "description": "The IDs of the queue items that were retried" + } + }, + "type": "object", + "required": ["queue_id", "retried_item_ids"], + "title": "RetryItemsResult" + }, + "RoundInvocation": { + "category": "math", + "class": "invocation", + "classification": "stable", + "description": "Rounds a float to a specified number of decimal places.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "value": { + "default": 0, + "description": "The float value", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Value", + "type": "number" + }, + "decimals": { + "default": 0, + "description": "The number of decimal places", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Decimals", + "type": "integer" + }, + "type": { + "const": "round_float", + "default": "round_float", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["math", "round"], + "title": "Round Float", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/FloatOutput" + } + }, + "SAMPoint": { + "properties": { + "x": { + "description": "The x-coordinate of the point", + "title": "X", + "type": "integer" + }, + "y": { + "description": "The y-coordinate of the point", + "title": "Y", + "type": "integer" + }, + "label": { + "$ref": "#/components/schemas/SAMPointLabel", + "description": "The label of the point" + } + }, + "required": ["x", "y", "label"], + "title": "SAMPoint", + "type": "object" + }, + "SAMPointLabel": { + "enum": [-1, 0, 1], + "title": "SAMPointLabel", + "type": "integer" + }, + "SAMPointsField": { + "properties": { + "points": { + "description": "The points of the object", + "items": { + "$ref": "#/components/schemas/SAMPoint" + }, + "minItems": 1, + "title": "Points", + "type": "array" + } + }, + "required": ["points"], + "title": "SAMPointsField", + "type": "object" + }, + "SD3ConditioningField": { + "description": "A conditioning tensor primitive value", + "properties": { + "conditioning_name": { + "description": "The name of conditioning tensor", + "title": "Conditioning Name", + "type": "string" + } + }, + "required": ["conditioning_name"], + "title": "SD3ConditioningField", + "type": "object" + }, + "SD3ConditioningOutput": { + "class": "output", + "description": "Base class for nodes that output a single SD3 conditioning tensor", + "properties": { + "conditioning": { + "$ref": "#/components/schemas/SD3ConditioningField", + "description": "Conditioning tensor", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "sd3_conditioning_output", + "default": "sd3_conditioning_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "conditioning", "type", "type"], + "title": "SD3ConditioningOutput", + "type": "object" + }, + "SD3DenoiseInvocation": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Run denoising process with a SD3 model.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "noise": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Noise tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoise_mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/DenoiseMaskField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "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.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoising_start": { + "default": 0.0, + "description": "When to start denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0.0, + "orig_required": false, + "title": "Denoising Start", + "type": "number" + }, + "denoising_end": { + "default": 1.0, + "description": "When to stop denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Denoising End", + "type": "number" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "SD3 model (MMDiTX) to load", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Transformer" + }, + "positive_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/SD3ConditioningField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Positive conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "negative_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/SD3ConditioningField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Negative conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "cfg_scale": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 3.5, + "description": "Classifier-Free Guidance scale", + "field_kind": "input", + "input": "any", + "orig_default": 3.5, + "orig_required": false, + "title": "CFG Scale" + }, + "width": { + "default": 1024, + "description": "Width of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 16, + "orig_default": 1024, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 1024, + "description": "Height of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 16, + "orig_default": 1024, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "steps": { + "default": 10, + "description": "Number of steps to run", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 10, + "orig_required": false, + "title": "Steps", + "type": "integer" + }, + "seed": { + "default": 0, + "description": "Randomness seed for reproducibility.", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Seed", + "type": "integer" + }, + "type": { + "const": "sd3_denoise", + "default": "sd3_denoise", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "sd3"], + "title": "Denoise - SD3", + "type": "object", + "version": "1.2.0", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "SD3ImageToLatentsInvocation": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Generates latents from an image.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to encode", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "type": { + "const": "sd3_i2l", + "default": "sd3_i2l", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "latents", "vae", "i2l", "sd3"], + "title": "Image to Latents - SD3", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "SD3LatentsToImageInvocation": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Generates an image from latents.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "type": { + "const": "sd3_l2i", + "default": "sd3_l2i", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "image", "vae", "l2i", "sd3"], + "title": "Latents to Image - SD3", + "type": "object", + "version": "1.3.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "SDXLCompelPromptInvocation": { + "category": "prompt", + "class": "invocation", + "classification": "stable", + "description": "Parse prompt using compel package to conditioning.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "prompt": { + "default": "", + "description": "Prompt to be parsed by Compel to create a conditioning tensor", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Prompt", + "type": "string", + "ui_component": "textarea" + }, + "style": { + "default": "", + "description": "Prompt to be parsed by Compel to create a conditioning tensor", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Style", + "type": "string", + "ui_component": "textarea" + }, + "original_width": { + "default": 1024, + "description": "", + "field_kind": "input", + "input": "any", + "orig_default": 1024, + "orig_required": false, + "title": "Original Width", + "type": "integer" + }, + "original_height": { + "default": 1024, + "description": "", + "field_kind": "input", + "input": "any", + "orig_default": 1024, + "orig_required": false, + "title": "Original Height", + "type": "integer" + }, + "crop_top": { + "default": 0, + "description": "", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Crop Top", + "type": "integer" + }, + "crop_left": { + "default": 0, + "description": "", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Crop Left", + "type": "integer" + }, + "target_width": { + "default": 1024, + "description": "", + "field_kind": "input", + "input": "any", + "orig_default": 1024, + "orig_required": false, + "title": "Target Width", + "type": "integer" + }, + "target_height": { + "default": 1024, + "description": "", + "field_kind": "input", + "input": "any", + "orig_default": 1024, + "orig_required": false, + "title": "Target Height", + "type": "integer" + }, + "clip": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "CLIP 1" + }, + "clip2": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "CLIP 2" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A mask defining the region that this conditioning prompt applies to.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "type": { + "const": "sdxl_compel_prompt", + "default": "sdxl_compel_prompt", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["sdxl", "compel", "prompt"], + "title": "Prompt - SDXL", + "type": "object", + "version": "1.2.1", + "output": { + "$ref": "#/components/schemas/ConditioningOutput" + } + }, + "SDXLLoRACollectionLoader": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Applies a collection of SDXL LoRAs to the provided UNet and CLIP models.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "loras": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoRAField" + }, + { + "items": { + "$ref": "#/components/schemas/LoRAField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LoRA models and weights. May be a single LoRA or collection.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "LoRAs" + }, + "unet": { + "anyOf": [ + { + "$ref": "#/components/schemas/UNetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "UNet (scheduler, LoRAs)", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "UNet" + }, + "clip": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "CLIP" + }, + "clip2": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "CLIP 2" + }, + "type": { + "const": "sdxl_lora_collection_loader", + "default": "sdxl_lora_collection_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["model"], + "title": "Apply LoRA Collection - SDXL", + "type": "object", + "version": "1.1.2", + "output": { + "$ref": "#/components/schemas/SDXLLoRALoaderOutput" + } + }, + "SDXLLoRALoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Apply selected lora to unet and text_encoder.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "lora": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LoRA model to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "LoRA", + "ui_model_base": ["sdxl"], + "ui_model_type": ["lora"] + }, + "weight": { + "default": 0.75, + "description": "The weight at which the LoRA is applied to each model", + "field_kind": "input", + "input": "any", + "orig_default": 0.75, + "orig_required": false, + "title": "Weight", + "type": "number" + }, + "unet": { + "anyOf": [ + { + "$ref": "#/components/schemas/UNetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "UNet (scheduler, LoRAs)", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "UNet" + }, + "clip": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "CLIP 1" + }, + "clip2": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "CLIP 2" + }, + "type": { + "const": "sdxl_lora_loader", + "default": "sdxl_lora_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["lora", "model"], + "title": "Apply LoRA - SDXL", + "type": "object", + "version": "1.0.5", + "output": { + "$ref": "#/components/schemas/SDXLLoRALoaderOutput" + } + }, + "SDXLLoRALoaderOutput": { + "class": "output", + "description": "SDXL LoRA Loader Output", + "properties": { + "unet": { + "anyOf": [ + { + "$ref": "#/components/schemas/UNetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "UNet (scheduler, LoRAs)", + "field_kind": "output", + "title": "UNet", + "ui_hidden": false + }, + "clip": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "output", + "title": "CLIP 1", + "ui_hidden": false + }, + "clip2": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "output", + "title": "CLIP 2", + "ui_hidden": false + }, + "type": { + "const": "sdxl_lora_loader_output", + "default": "sdxl_lora_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "unet", "clip", "clip2", "type", "type"], + "title": "SDXLLoRALoaderOutput", + "type": "object" + }, + "SDXLModelLoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Loads an sdxl base model, outputting its submodels.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "SDXL Main model (UNet, VAE, CLIP1, CLIP2) to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "ui_model_base": ["sdxl"], + "ui_model_type": ["main"] + }, + "type": { + "const": "sdxl_model_loader", + "default": "sdxl_model_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["model", "sdxl"], + "title": "Main Model - SDXL", + "type": "object", + "version": "1.0.4", + "output": { + "$ref": "#/components/schemas/SDXLModelLoaderOutput" + } + }, + "SDXLModelLoaderOutput": { + "class": "output", + "description": "SDXL base model loader output", + "properties": { + "unet": { + "$ref": "#/components/schemas/UNetField", + "description": "UNet (scheduler, LoRAs)", + "field_kind": "output", + "title": "UNet", + "ui_hidden": false + }, + "clip": { + "$ref": "#/components/schemas/CLIPField", + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "output", + "title": "CLIP 1", + "ui_hidden": false + }, + "clip2": { + "$ref": "#/components/schemas/CLIPField", + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "output", + "title": "CLIP 2", + "ui_hidden": false + }, + "vae": { + "$ref": "#/components/schemas/VAEField", + "description": "VAE", + "field_kind": "output", + "title": "VAE", + "ui_hidden": false + }, + "type": { + "const": "sdxl_model_loader_output", + "default": "sdxl_model_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "unet", "clip", "clip2", "vae", "type", "type"], + "title": "SDXLModelLoaderOutput", + "type": "object" + }, + "SDXLRefinerCompelPromptInvocation": { + "category": "prompt", + "class": "invocation", + "classification": "stable", + "description": "Parse prompt using compel package to conditioning.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "style": { + "default": "", + "description": "Prompt to be parsed by Compel to create a conditioning tensor", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Style", + "type": "string", + "ui_component": "textarea" + }, + "original_width": { + "default": 1024, + "description": "", + "field_kind": "input", + "input": "any", + "orig_default": 1024, + "orig_required": false, + "title": "Original Width", + "type": "integer" + }, + "original_height": { + "default": 1024, + "description": "", + "field_kind": "input", + "input": "any", + "orig_default": 1024, + "orig_required": false, + "title": "Original Height", + "type": "integer" + }, + "crop_top": { + "default": 0, + "description": "", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Crop Top", + "type": "integer" + }, + "crop_left": { + "default": 0, + "description": "", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Crop Left", + "type": "integer" + }, + "aesthetic_score": { + "default": 6.0, + "description": "The aesthetic score to apply to the conditioning tensor", + "field_kind": "input", + "input": "any", + "orig_default": 6.0, + "orig_required": false, + "title": "Aesthetic Score", + "type": "number" + }, + "clip2": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "type": { + "const": "sdxl_refiner_compel_prompt", + "default": "sdxl_refiner_compel_prompt", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["sdxl", "compel", "prompt"], + "title": "Prompt - SDXL Refiner", + "type": "object", + "version": "1.1.2", + "output": { + "$ref": "#/components/schemas/ConditioningOutput" + } + }, + "SDXLRefinerModelLoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Loads an sdxl refiner model, outputting its submodels.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "SDXL Refiner Main Modde (UNet, VAE, CLIP2) to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "ui_model_base": ["sdxl-refiner"], + "ui_model_type": ["main"] + }, + "type": { + "const": "sdxl_refiner_model_loader", + "default": "sdxl_refiner_model_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["model", "sdxl", "refiner"], + "title": "Refiner Model - SDXL", + "type": "object", + "version": "1.0.4", + "output": { + "$ref": "#/components/schemas/SDXLRefinerModelLoaderOutput" + } + }, + "SDXLRefinerModelLoaderOutput": { + "class": "output", + "description": "SDXL refiner model loader output", + "properties": { + "unet": { + "$ref": "#/components/schemas/UNetField", + "description": "UNet (scheduler, LoRAs)", + "field_kind": "output", + "title": "UNet", + "ui_hidden": false + }, + "clip2": { + "$ref": "#/components/schemas/CLIPField", + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "output", + "title": "CLIP 2", + "ui_hidden": false + }, + "vae": { + "$ref": "#/components/schemas/VAEField", + "description": "VAE", + "field_kind": "output", + "title": "VAE", + "ui_hidden": false + }, + "type": { + "const": "sdxl_refiner_model_loader_output", + "default": "sdxl_refiner_model_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "unet", "clip2", "vae", "type", "type"], + "title": "SDXLRefinerModelLoaderOutput", + "type": "object" + }, + "SQLiteDirection": { + "type": "string", + "enum": ["ASC", "DESC"], + "title": "SQLiteDirection" + }, + "SaveImageInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Saves an image. Unlike an image primitive, this invocation stores a copy of the image.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": false, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to process", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "save_image", + "default": "save_image", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "image"], + "title": "Save Image", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "SaveImageToFileInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Saves an image to the gallery (like the standard Save Image node) AND additionally exports a copy\nto the filesystem with a custom filename.\n\nFilename pattern: {prefix}{uuid}{suffix}.{file_format}\n- The UUID is the same UUID used for the gallery entry, so the exported file can be matched to the gallery item.\n- The gallery entry itself always uses the plain UUID (prefix/suffix apply only to the exported file on disk).\n- Board and Metadata inputs behave exactly like the standard Save Image node.\n- The export target is restricted to (subfolders of) the InvokeAI outputs folder \u2014 absolute paths are rejected.\n\nExample: prefix=\"hero_\", suffix=\"_final\", file_format=\"png\" \u2192 \"hero__final.png\"", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": false, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to save and export", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "output_directory": { + "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' \u2192 /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.", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Output Directory", + "type": "string" + }, + "prefix": { + "default": "", + "description": "Text prepended to the UUID in the exported filename. Example: 'portrait_' \u2192 'portrait_.png'", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Prefix", + "type": "string" + }, + "suffix": { + "default": "", + "description": "Text appended to the UUID (before the extension). Example: '_v2' \u2192 '_v2.png'", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Suffix", + "type": "string" + }, + "file_format": { + "default": "png", + "description": "File format for the exported file. PNG is lossless; JPG/WEBP are lossy and respect 'quality'.", + "enum": ["png", "jpg", "webp"], + "field_kind": "input", + "input": "any", + "orig_default": "png", + "orig_required": false, + "title": "File Format", + "type": "string" + }, + "quality": { + "default": 95, + "description": "Compression quality for JPG and WEBP (1-100, higher = better quality, larger file). Ignored for PNG.", + "field_kind": "input", + "input": "any", + "maximum": 100, + "minimum": 1, + "orig_default": 95, + "orig_required": false, + "title": "Quality", + "type": "integer" + }, + "type": { + "const": "save_image_to_file", + "default": "save_image_to_file", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "export", "file", "save"], + "title": "Save Image (Gallery + File Export)", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ScaleLatentsInvocation": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Scales latents by a given factor.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "scale_factor": { + "anyOf": [ + { + "exclusiveMinimum": 0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The factor by which to scale", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Scale Factor" + }, + "mode": { + "default": "bilinear", + "description": "Interpolation mode", + "enum": ["nearest", "linear", "bilinear", "bicubic", "trilinear", "area", "nearest-exact"], + "field_kind": "input", + "input": "any", + "orig_default": "bilinear", + "orig_required": false, + "title": "Mode", + "type": "string" + }, + "antialias": { + "default": false, + "description": "Whether or not to apply antialiasing (bilinear or bicubic only)", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Antialias", + "type": "boolean" + }, + "type": { + "const": "lscale", + "default": "lscale", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "resize"], + "title": "Scale Latents", + "type": "object", + "version": "1.0.2", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "SchedulerInvocation": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Selects a scheduler.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "scheduler": { + "default": "euler", + "description": "Scheduler to use during inference", + "enum": [ + "ddim", + "ddpm", + "deis", + "deis_k", + "lms", + "lms_k", + "pndm", + "heun", + "heun_k", + "euler", + "euler_k", + "euler_a", + "kdpm_2", + "kdpm_2_k", + "kdpm_2_a", + "kdpm_2_a_k", + "dpmpp_2s", + "dpmpp_2s_k", + "dpmpp_2m", + "dpmpp_2m_k", + "dpmpp_2m_sde", + "dpmpp_2m_sde_k", + "dpmpp_3m", + "dpmpp_3m_k", + "dpmpp_sde", + "dpmpp_sde_k", + "er_sde", + "unipc", + "unipc_k", + "lcm", + "tcd" + ], + "field_kind": "input", + "input": "any", + "orig_default": "euler", + "orig_required": false, + "title": "Scheduler", + "type": "string", + "ui_type": "SchedulerField" + }, + "type": { + "const": "scheduler", + "default": "scheduler", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["scheduler"], + "title": "Scheduler", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/SchedulerOutput" + } + }, + "SchedulerOutput": { + "class": "output", + "properties": { + "scheduler": { + "description": "Scheduler to use during inference", + "enum": [ + "ddim", + "ddpm", + "deis", + "deis_k", + "lms", + "lms_k", + "pndm", + "heun", + "heun_k", + "euler", + "euler_k", + "euler_a", + "kdpm_2", + "kdpm_2_k", + "kdpm_2_a", + "kdpm_2_a_k", + "dpmpp_2s", + "dpmpp_2s_k", + "dpmpp_2m", + "dpmpp_2m_k", + "dpmpp_2m_sde", + "dpmpp_2m_sde_k", + "dpmpp_3m", + "dpmpp_3m_k", + "dpmpp_sde", + "dpmpp_sde_k", + "er_sde", + "unipc", + "unipc_k", + "lcm", + "tcd" + ], + "field_kind": "output", + "title": "Scheduler", + "type": "string", + "ui_hidden": false, + "ui_type": "SchedulerField" + }, + "type": { + "const": "scheduler_output", + "default": "scheduler_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "scheduler", "type", "type"], + "title": "SchedulerOutput", + "type": "object" + }, + "SchedulerPredictionType": { + "type": "string", + "enum": ["epsilon", "v_prediction", "sample"], + "title": "SchedulerPredictionType", + "description": "Scheduler prediction type." + }, + "Sd3ModelLoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Loads a SD3 base model, outputting its submodels.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "SD3 model (MMDiTX) to load", + "field_kind": "input", + "input": "direct", + "orig_required": true, + "ui_model_base": ["sd-3"], + "ui_model_type": ["main"] + }, + "t5_encoder_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "T5 tokenizer and text encoder", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "T5 Encoder", + "ui_model_type": ["t5_encoder"] + }, + "clip_l_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP Embed loader", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "CLIP L Encoder", + "ui_model_type": ["clip_embed"], + "ui_model_variant": ["large"] + }, + "clip_g_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP-G Embed loader", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "CLIP G Encoder", + "ui_model_type": ["clip_embed"], + "ui_model_variant": ["gigantic"] + }, + "vae_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE model to load", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "VAE", + "ui_model_base": ["sd-3"], + "ui_model_type": ["vae"] + }, + "type": { + "const": "sd3_model_loader", + "default": "sd3_model_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["model", "type", "id"], + "tags": ["model", "sd3"], + "title": "Main Model - SD3", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/Sd3ModelLoaderOutput" + } + }, + "Sd3ModelLoaderOutput": { + "class": "output", + "description": "SD3 base model loader output.", + "properties": { + "transformer": { + "$ref": "#/components/schemas/TransformerField", + "description": "Transformer", + "field_kind": "output", + "title": "Transformer", + "ui_hidden": false + }, + "clip_l": { + "$ref": "#/components/schemas/CLIPField", + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "output", + "title": "CLIP L", + "ui_hidden": false + }, + "clip_g": { + "$ref": "#/components/schemas/CLIPField", + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "output", + "title": "CLIP G", + "ui_hidden": false + }, + "t5_encoder": { + "$ref": "#/components/schemas/T5EncoderField", + "description": "T5 tokenizer and text encoder", + "field_kind": "output", + "title": "T5 Encoder", + "ui_hidden": false + }, + "vae": { + "$ref": "#/components/schemas/VAEField", + "description": "VAE", + "field_kind": "output", + "title": "VAE", + "ui_hidden": false + }, + "type": { + "const": "sd3_model_loader_output", + "default": "sd3_model_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "transformer", "clip_l", "clip_g", "t5_encoder", "vae", "type", "type"], + "title": "Sd3ModelLoaderOutput", + "type": "object" + }, + "Sd3TextEncoderInvocation": { + "category": "prompt", + "class": "invocation", + "classification": "stable", + "description": "Encodes and preps a prompt for a SD3 image.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "clip_l": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "CLIP L" + }, + "clip_g": { + "anyOf": [ + { + "$ref": "#/components/schemas/CLIPField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "CLIP G" + }, + "t5_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/T5EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "T5 tokenizer and text encoder", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "T5Encoder" + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Text prompt to encode.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Prompt" + }, + "type": { + "const": "sd3_text_encoder", + "default": "sd3_text_encoder", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["prompt", "conditioning", "sd3"], + "title": "Prompt - SD3", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/SD3ConditioningOutput" + } + }, + "SeamlessModeInvocation": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Applies the seamless transformation to the Model UNet and VAE.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "unet": { + "anyOf": [ + { + "$ref": "#/components/schemas/UNetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "UNet (scheduler, LoRAs)", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "UNet" + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE model to load", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "VAE" + }, + "seamless_y": { + "default": true, + "description": "Specify whether Y axis is seamless", + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "Seamless Y", + "type": "boolean" + }, + "seamless_x": { + "default": true, + "description": "Specify whether X axis is seamless", + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "Seamless X", + "type": "boolean" + }, + "type": { + "const": "seamless", + "default": "seamless", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["seamless", "model"], + "title": "Apply Seamless - SD1.5, SDXL", + "type": "object", + "version": "1.0.2", + "output": { + "$ref": "#/components/schemas/SeamlessModeOutput" + } + }, + "SeamlessModeOutput": { + "class": "output", + "description": "Modified Seamless Model output", + "properties": { + "unet": { + "anyOf": [ + { + "$ref": "#/components/schemas/UNetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "UNet (scheduler, LoRAs)", + "field_kind": "output", + "title": "UNet", + "ui_hidden": false + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "output", + "title": "VAE", + "ui_hidden": false + }, + "type": { + "const": "seamless_output", + "default": "seamless_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "unet", "vae", "type", "type"], + "title": "SeamlessModeOutput", + "type": "object" + }, + "SeedreamImageGenerationInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Generate images using a BytePlus Seedream model.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Main model (UNet, VAE, CLIP) to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "ui_model_base": ["external"], + "ui_model_format": ["external_api"], + "ui_model_provider_id": ["seedream"], + "ui_model_type": ["external_image_generator"] + }, + "mode": { + "default": "txt2img", + "description": "Generation mode.", + "enum": ["txt2img", "img2img", "inpaint"], + "field_kind": "input", + "input": "any", + "orig_default": "txt2img", + "orig_required": false, + "title": "Mode", + "type": "string", + "ui_hidden": true + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Prompt", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Prompt" + }, + "seed": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Seed for random number generation", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Seed" + }, + "num_images": { + "default": 1, + "description": "Number of images to generate", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 1, + "orig_required": false, + "title": "Num Images", + "type": "integer" + }, + "width": { + "default": 1024, + "description": "Width of output (px)", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 1024, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 1024, + "description": "Height of output (px)", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 1024, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "image_size": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Image size preset (e.g. 1K, 2K, 4K)", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Image Size" + }, + "init_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Init image for img2img/inpaint", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "mask_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Mask image for inpaint", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "ui_hidden": true + }, + "reference_images": { + "default": [], + "description": "Reference images", + "field_kind": "input", + "input": "any", + "items": { + "$ref": "#/components/schemas/ImageField" + }, + "orig_default": [], + "orig_required": false, + "title": "Reference Images", + "type": "array" + }, + "watermark": { + "default": false, + "description": "Add watermark to generated images", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Watermark", + "type": "boolean" + }, + "optimize_prompt": { + "default": false, + "description": "Let the model optimize the prompt before generation", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Optimize Prompt", + "type": "boolean" + }, + "type": { + "const": "seedream_image_generation", + "default": "seedream_image_generation", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["external", "generation", "seedream"], + "title": "Seedream Image Generation", + "type": "object", + "version": "1.1.0", + "output": { + "$ref": "#/components/schemas/ImageCollectionOutput" + } + }, + "SegmentAnythingInvocation": { + "category": "segmentation", + "class": "invocation", + "classification": "stable", + "description": "Runs a Segment Anything Model (SAM or SAM2).", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "anyOf": [ + { + "enum": [ + "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" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The Segment Anything model to use (SAM or SAM2).", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Model" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to segment.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "bounding_boxes": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/BoundingBoxField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The bounding boxes to prompt the model with.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Bounding Boxes" + }, + "point_lists": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/SAMPointsField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The list of point lists to prompt the model with. Each list of points represents a single object.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Point Lists" + }, + "apply_polygon_refinement": { + "default": true, + "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).", + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "Apply Polygon Refinement", + "type": "boolean" + }, + "mask_filter": { + "default": "all", + "description": "The filtering to apply to the detected masks before merging them into a final output.", + "enum": ["all", "largest", "highest_box_score"], + "field_kind": "input", + "input": "any", + "orig_default": "all", + "orig_required": false, + "title": "Mask Filter", + "type": "string" + }, + "type": { + "const": "segment_anything", + "default": "segment_anything", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["prompt", "segmentation", "sam", "sam2"], + "title": "Segment Anything", + "type": "object", + "version": "1.3.0", + "output": { + "$ref": "#/components/schemas/MaskOutput" + } + }, + "SessionProcessorStatus": { + "properties": { + "is_started": { + "type": "boolean", + "title": "Is Started", + "description": "Whether the session processor is started" + }, + "is_processing": { + "type": "boolean", + "title": "Is Processing", + "description": "Whether a session is being processed" + } + }, + "type": "object", + "required": ["is_started", "is_processing"], + "title": "SessionProcessorStatus" + }, + "SessionQueueAndProcessorStatus": { + "properties": { + "queue": { + "$ref": "#/components/schemas/SessionQueueStatus" + }, + "processor": { + "$ref": "#/components/schemas/SessionProcessorStatus" + } + }, + "type": "object", + "required": ["queue", "processor"], + "title": "SessionQueueAndProcessorStatus", + "description": "The overall status of session queue and processor" + }, + "SessionQueueCountsByDestination": { + "properties": { + "queue_id": { + "type": "string", + "title": "Queue Id", + "description": "The ID of the queue" + }, + "destination": { + "type": "string", + "title": "Destination", + "description": "The destination of queue items included in this status" + }, + "pending": { + "type": "integer", + "title": "Pending", + "description": "Number of queue items with status 'pending' for the destination" + }, + "in_progress": { + "type": "integer", + "title": "In Progress", + "description": "Number of queue items with status 'in_progress' for the destination" + }, + "completed": { + "type": "integer", + "title": "Completed", + "description": "Number of queue items with status 'complete' for the destination" + }, + "failed": { + "type": "integer", + "title": "Failed", + "description": "Number of queue items with status 'error' for the destination" + }, + "canceled": { + "type": "integer", + "title": "Canceled", + "description": "Number of queue items with status 'canceled' for the destination" + }, + "total": { + "type": "integer", + "title": "Total", + "description": "Total number of queue items for the destination" + } + }, + "type": "object", + "required": ["queue_id", "destination", "pending", "in_progress", "completed", "failed", "canceled", "total"], + "title": "SessionQueueCountsByDestination" + }, + "SessionQueueItem": { + "properties": { + "item_id": { + "type": "integer", + "title": "Item Id", + "description": "The identifier of the session queue item" + }, + "status": { + "type": "string", + "enum": ["pending", "in_progress", "completed", "failed", "canceled"], + "title": "Status", + "description": "The status of this queue item", + "default": "pending" + }, + "status_sequence": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Status Sequence", + "description": "A monotonically increasing version for this queue item's visible status lifecycle" + }, + "priority": { + "type": "integer", + "title": "Priority", + "description": "The priority of this queue item", + "default": 0 + }, + "batch_id": { + "type": "string", + "title": "Batch Id", + "description": "The ID of the batch associated with this queue item" + }, + "origin": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Origin", + "description": "The origin of this queue item. This data is used by the frontend to determine how to handle results." + }, + "destination": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Destination", + "description": "The origin of this queue item. This data is used by the frontend to determine how to handle results" + }, + "session_id": { + "type": "string", + "title": "Session Id", + "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": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Type", + "description": "The error type if this queue item errored" + }, + "error_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Message", + "description": "The error message if this queue item errored" + }, + "error_traceback": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Traceback", + "description": "The error traceback if this queue item errored" + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + } + ], + "title": "Created At", + "description": "When this queue item was created" + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + } + ], + "title": "Updated At", + "description": "When this queue item was updated" + }, + "started_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Started At", + "description": "When this queue item was started" + }, + "completed_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Completed At", + "description": "When this queue item was completed" + }, + "queue_id": { + "type": "string", + "title": "Queue Id", + "description": "The id of the queue with which this item is associated" + }, + "user_id": { + "type": "string", + "title": "User Id", + "description": "The id of the user who created this queue item", + "default": "system" + }, + "user_display_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User Display Name", + "description": "The display name of the user who created this queue item, if available" + }, + "user_email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User Email", + "description": "The email of the user who created this queue item, if available" + }, + "field_values": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/NodeFieldValue" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Field Values", + "description": "The field values that were used for this queue item" + }, + "retried_from_item_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Retried From Item Id", + "description": "The item_id of the queue item that this item was retried from" + }, + "session": { + "$ref": "#/components/schemas/GraphExecutionState", + "description": "The fully-populated session to be executed" + }, + "workflow": { + "anyOf": [ + { + "$ref": "#/components/schemas/WorkflowWithoutID" + }, + { + "type": "null" + } + ], + "description": "The workflow associated with this queue item" + } + }, + "type": "object", + "required": [ + "item_id", + "status", + "batch_id", + "queue_id", + "session_id", + "session", + "priority", + "session_id", + "created_at", + "updated_at" + ], + "title": "SessionQueueItem", + "description": "Session queue item without the full graph. Used for serialization." + }, + "SessionQueueStatus": { + "properties": { + "queue_id": { + "type": "string", + "title": "Queue Id", + "description": "The ID of the queue" + }, + "item_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Item Id", + "description": "The current queue item id" + }, + "batch_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Batch Id", + "description": "The current queue item's batch id" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Session Id", + "description": "The current queue item's session id" + }, + "pending": { + "type": "integer", + "title": "Pending", + "description": "Number of queue items with status 'pending'" + }, + "in_progress": { + "type": "integer", + "title": "In Progress", + "description": "Number of queue items with status 'in_progress'" + }, + "completed": { + "type": "integer", + "title": "Completed", + "description": "Number of queue items with status 'complete'" + }, + "failed": { + "type": "integer", + "title": "Failed", + "description": "Number of queue items with status 'error'" + }, + "canceled": { + "type": "integer", + "title": "Canceled", + "description": "Number of queue items with status 'canceled'" + }, + "total": { + "type": "integer", + "title": "Total", + "description": "Total number of queue items" + } + }, + "type": "object", + "required": [ + "queue_id", + "item_id", + "batch_id", + "session_id", + "pending", + "in_progress", + "completed", + "failed", + "canceled", + "total" + ], + "title": "SessionQueueStatus" + }, + "SetupRequest": { + "properties": { + "email": { + "type": "string", + "title": "Email", + "description": "Admin email address" + }, + "display_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Display Name", + "description": "Admin display name" + }, + "password": { + "type": "string", + "title": "Password", + "description": "Admin password" + } + }, + "type": "object", + "required": ["email", "password"], + "title": "SetupRequest", + "description": "Request body for initial admin setup." + }, + "SetupResponse": { + "properties": { + "success": { + "type": "boolean", + "title": "Success", + "description": "Whether setup was successful" + }, + "user": { + "$ref": "#/components/schemas/UserDTO", + "description": "Created admin user information" + } + }, + "type": "object", + "required": ["success", "user"], + "title": "SetupResponse", + "description": "Response from successful admin setup." + }, + "SetupStatusResponse": { + "properties": { + "setup_required": { + "type": "boolean", + "title": "Setup Required", + "description": "Whether initial setup is required" + }, + "multiuser_enabled": { + "type": "boolean", + "title": "Multiuser Enabled", + "description": "Whether multiuser mode is enabled" + }, + "strict_password_checking": { + "type": "boolean", + "title": "Strict Password Checking", + "description": "Whether strict password requirements are enforced" + }, + "admin_email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Admin Email", + "description": "Email of the first active admin user, if any" + } + }, + "type": "object", + "required": ["setup_required", "multiuser_enabled", "strict_password_checking"], + "title": "SetupStatusResponse", + "description": "Response for setup status check." + }, + "ShowImageInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Displays a provided image using the OS image viewer, and passes it forward in the pipeline.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to show", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "show_image", + "default": "show_image", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image"], + "title": "Show Image", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "SigLIP_Diffusers_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "type": { + "type": "string", + "const": "siglip", + "title": "Type", + "default": "siglip" + }, + "base": { + "type": "string", + "const": "any", + "title": "Base", + "default": "any" + }, + "cpu_only": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Cpu Only", + "description": "Whether this model should run on CPU only" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "format", + "repo_variant", + "type", + "base", + "cpu_only" + ], + "title": "SigLIP_Diffusers_Config", + "description": "Model config for SigLIP." + }, + "SpandrelImageToImageAutoscaleInvocation": { + "category": "upscale", + "class": "invocation", + "classification": "stable", + "description": "Run any spandrel image-to-image model (https://github.com/chaiNNer-org/spandrel) until the target scale is reached.", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The input image", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "image_to_image_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Image-to-Image model", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Image-to-Image Model", + "ui_model_type": ["spandrel_image_to_image"] + }, + "tile_size": { + "default": 512, + "description": "The tile size for tiled image-to-image. Set to 0 to disable tiling.", + "field_kind": "input", + "input": "any", + "orig_default": 512, + "orig_required": false, + "title": "Tile Size", + "type": "integer" + }, + "type": { + "const": "spandrel_image_to_image_autoscale", + "default": "spandrel_image_to_image_autoscale", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + }, + "scale": { + "default": 4.0, + "description": "The final scale of the output image. If the model does not upscale the image, this will be ignored.", + "exclusiveMinimum": 0.0, + "field_kind": "input", + "input": "any", + "maximum": 16.0, + "orig_default": 4.0, + "orig_required": false, + "title": "Scale", + "type": "number" + }, + "fit_to_multiple_of_8": { + "default": false, + "description": "If true, the output image will be resized to the nearest multiple of 8 in both dimensions.", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Fit To Multiple Of 8", + "type": "boolean" + } + }, + "required": ["type", "id"], + "tags": ["upscale"], + "title": "Image-to-Image (Autoscale)", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "SpandrelImageToImageInvocation": { + "category": "upscale", + "class": "invocation", + "classification": "stable", + "description": "Run any spandrel image-to-image model (https://github.com/chaiNNer-org/spandrel).", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The input image", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "image_to_image_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Image-to-Image model", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Image-to-Image Model", + "ui_model_type": ["spandrel_image_to_image"] + }, + "tile_size": { + "default": 512, + "description": "The tile size for tiled image-to-image. Set to 0 to disable tiling.", + "field_kind": "input", + "input": "any", + "orig_default": 512, + "orig_required": false, + "title": "Tile Size", + "type": "integer" + }, + "type": { + "const": "spandrel_image_to_image", + "default": "spandrel_image_to_image", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["upscale"], + "title": "Image-to-Image", + "type": "object", + "version": "1.3.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "Spandrel_Checkpoint_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "base": { + "type": "string", + "const": "any", + "title": "Base", + "default": "any" + }, + "type": { + "type": "string", + "const": "spandrel_image_to_image", + "title": "Type", + "default": "spandrel_image_to_image" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "base", + "type", + "format" + ], + "title": "Spandrel_Checkpoint_Config", + "description": "Model config for Spandrel Image to Image models." + }, + "StarredImagesResult": { + "properties": { + "affected_boards": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Affected Boards", + "description": "The ids of boards affected by the delete operation" + }, + "starred_images": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Starred Images", + "description": "The names of the images that were starred" + } + }, + "type": "object", + "required": ["affected_boards", "starred_images"], + "title": "StarredImagesResult" + }, + "StarterModel": { + "properties": { + "description": { + "type": "string", + "title": "Description" + }, + "source": { + "type": "string", + "title": "Source" + }, + "name": { + "type": "string", + "title": "Name" + }, + "base": { + "$ref": "#/components/schemas/BaseModelType" + }, + "type": { + "$ref": "#/components/schemas/ModelType" + }, + "format": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelFormat" + }, + { + "type": "null" + } + ] + }, + "variant": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelVariantType" + }, + { + "$ref": "#/components/schemas/ClipVariantType" + }, + { + "$ref": "#/components/schemas/FluxVariantType" + }, + { + "$ref": "#/components/schemas/Flux2VariantType" + }, + { + "$ref": "#/components/schemas/ZImageVariantType" + }, + { + "$ref": "#/components/schemas/QwenImageVariantType" + }, + { + "$ref": "#/components/schemas/Qwen3VariantType" + }, + { + "type": "null" + } + ], + "title": "Variant" + }, + "is_installed": { + "type": "boolean", + "title": "Is Installed", + "default": false + }, + "capabilities": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExternalModelCapabilities" + }, + { + "type": "null" + } + ] + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExternalApiModelDefaultSettings" + }, + { + "type": "null" + } + ] + }, + "panel_schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExternalModelPanelSchema" + }, + { + "type": "null" + } + ] + }, + "previous_names": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Previous Names", + "default": [] + }, + "dependencies": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/StarterModelWithoutDependencies" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Dependencies" + } + }, + "type": "object", + "required": ["description", "source", "name", "base", "type"], + "title": "StarterModel" + }, + "StarterModelBundle": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "models": { + "items": { + "$ref": "#/components/schemas/StarterModel" + }, + "type": "array", + "title": "Models" + } + }, + "type": "object", + "required": ["name", "models"], + "title": "StarterModelBundle" + }, + "StarterModelResponse": { + "properties": { + "starter_models": { + "items": { + "$ref": "#/components/schemas/StarterModel" + }, + "type": "array", + "title": "Starter Models" + }, + "starter_bundles": { + "additionalProperties": { + "$ref": "#/components/schemas/StarterModelBundle" + }, + "type": "object", + "title": "Starter Bundles" + } + }, + "type": "object", + "required": ["starter_models", "starter_bundles"], + "title": "StarterModelResponse" + }, + "StarterModelWithoutDependencies": { + "properties": { + "description": { + "type": "string", + "title": "Description" + }, + "source": { + "type": "string", + "title": "Source" + }, + "name": { + "type": "string", + "title": "Name" + }, + "base": { + "$ref": "#/components/schemas/BaseModelType" + }, + "type": { + "$ref": "#/components/schemas/ModelType" + }, + "format": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelFormat" + }, + { + "type": "null" + } + ] + }, + "variant": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelVariantType" + }, + { + "$ref": "#/components/schemas/ClipVariantType" + }, + { + "$ref": "#/components/schemas/FluxVariantType" + }, + { + "$ref": "#/components/schemas/Flux2VariantType" + }, + { + "$ref": "#/components/schemas/ZImageVariantType" + }, + { + "$ref": "#/components/schemas/QwenImageVariantType" + }, + { + "$ref": "#/components/schemas/Qwen3VariantType" + }, + { + "type": "null" + } + ], + "title": "Variant" + }, + "is_installed": { + "type": "boolean", + "title": "Is Installed", + "default": false + }, + "capabilities": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExternalModelCapabilities" + }, + { + "type": "null" + } + ] + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExternalApiModelDefaultSettings" + }, + { + "type": "null" + } + ] + }, + "panel_schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExternalModelPanelSchema" + }, + { + "type": "null" + } + ] + }, + "previous_names": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Previous Names", + "default": [] + } + }, + "type": "object", + "required": ["description", "source", "name", "base", "type"], + "title": "StarterModelWithoutDependencies" + }, + "String2Output": { + "class": "output", + "description": "Base class for invocations that output two strings", + "properties": { + "string_1": { + "description": "string 1", + "field_kind": "output", + "title": "String 1", + "type": "string", + "ui_hidden": false + }, + "string_2": { + "description": "string 2", + "field_kind": "output", + "title": "String 2", + "type": "string", + "ui_hidden": false + }, + "type": { + "const": "string_2_output", + "default": "string_2_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "string_1", "string_2", "type", "type"], + "title": "String2Output", + "type": "object" + }, + "StringBatchInvocation": { + "category": "batch", + "class": "invocation", + "classification": "special", + "description": "Create a batched generation, where the workflow is executed once for each string in the batch.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "batch_group_id": { + "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.", + "enum": ["None", "Group 1", "Group 2", "Group 3", "Group 4", "Group 5"], + "field_kind": "input", + "input": "direct", + "orig_default": "None", + "orig_required": false, + "title": "Batch Group", + "type": "string" + }, + "strings": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The strings to batch over", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Strings" + }, + "type": { + "const": "string_batch", + "default": "string_batch", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "string", "batch", "special"], + "title": "String Batch", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/StringOutput" + } + }, + "StringCollectionInvocation": { + "category": "primitives", + "class": "invocation", + "classification": "stable", + "description": "A collection of string primitive values", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "collection": { + "default": [], + "description": "The collection of string values", + "field_kind": "input", + "input": "any", + "items": { + "type": "string" + }, + "orig_default": [], + "orig_required": false, + "title": "Collection", + "type": "array" + }, + "type": { + "const": "string_collection", + "default": "string_collection", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "string", "collection"], + "title": "String Collection Primitive", + "type": "object", + "version": "1.0.2", + "output": { + "$ref": "#/components/schemas/StringCollectionOutput" + } + }, + "StringCollectionOutput": { + "class": "output", + "description": "Base class for nodes that output a collection of strings", + "properties": { + "collection": { + "description": "The output strings", + "field_kind": "output", + "items": { + "type": "string" + }, + "title": "Collection", + "type": "array", + "ui_hidden": false + }, + "type": { + "const": "string_collection_output", + "default": "string_collection_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "collection", "type", "type"], + "title": "StringCollectionOutput", + "type": "object" + }, + "StringGenerator": { + "category": "batch", + "class": "invocation", + "classification": "special", + "description": "Generated a range of strings for use in a batched generation", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "generator": { + "$ref": "#/components/schemas/StringGeneratorField", + "description": "The string generator.", + "field_kind": "input", + "input": "direct", + "orig_required": true, + "title": "Generator Type" + }, + "type": { + "const": "string_generator", + "default": "string_generator", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["generator", "type", "id"], + "tags": ["primitives", "string", "number", "batch", "special"], + "title": "String Generator", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/StringGeneratorOutput" + } + }, + "StringGeneratorField": { + "properties": {}, + "title": "StringGeneratorField", + "type": "object" + }, + "StringGeneratorOutput": { + "class": "output", + "description": "Base class for nodes that output a collection of strings", + "properties": { + "strings": { + "description": "The generated strings", + "field_kind": "output", + "items": { + "type": "string" + }, + "title": "Strings", + "type": "array", + "ui_hidden": false + }, + "type": { + "const": "string_generator_output", + "default": "string_generator_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "strings", "type", "type"], + "title": "StringGeneratorOutput", + "type": "object" + }, + "StringInvocation": { + "category": "primitives", + "class": "invocation", + "classification": "stable", + "description": "A string primitive value", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "value": { + "default": "", + "description": "The string value", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Value", + "type": "string", + "ui_component": "textarea" + }, + "type": { + "const": "string", + "default": "string", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["primitives", "string"], + "title": "String Primitive", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/StringOutput" + } + }, + "StringJoinInvocation": { + "category": "strings", + "class": "invocation", + "classification": "stable", + "description": "Joins string left to string right", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "string_left": { + "default": "", + "description": "String Left", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "String Left", + "type": "string", + "ui_component": "textarea" + }, + "string_right": { + "default": "", + "description": "String Right", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "String Right", + "type": "string", + "ui_component": "textarea" + }, + "type": { + "const": "string_join", + "default": "string_join", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["string", "join"], + "title": "String Join", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/StringOutput" + } + }, + "StringJoinThreeInvocation": { + "category": "strings", + "class": "invocation", + "classification": "stable", + "description": "Joins string left to string middle to string right", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "string_left": { + "default": "", + "description": "String Left", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "String Left", + "type": "string", + "ui_component": "textarea" + }, + "string_middle": { + "default": "", + "description": "String Middle", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "String Middle", + "type": "string", + "ui_component": "textarea" + }, + "string_right": { + "default": "", + "description": "String Right", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "String Right", + "type": "string", + "ui_component": "textarea" + }, + "type": { + "const": "string_join_three", + "default": "string_join_three", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["string", "join"], + "title": "String Join Three", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/StringOutput" + } + }, + "StringOutput": { + "class": "output", + "description": "Base class for nodes that output a single string", + "properties": { + "value": { + "description": "The output string", + "field_kind": "output", + "title": "Value", + "type": "string", + "ui_hidden": false + }, + "type": { + "const": "string_output", + "default": "string_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "value", "type", "type"], + "title": "StringOutput", + "type": "object" + }, + "StringPosNegOutput": { + "class": "output", + "description": "Base class for invocations that output a positive and negative string", + "properties": { + "positive_string": { + "description": "Positive string", + "field_kind": "output", + "title": "Positive String", + "type": "string", + "ui_hidden": false + }, + "negative_string": { + "description": "Negative string", + "field_kind": "output", + "title": "Negative String", + "type": "string", + "ui_hidden": false + }, + "type": { + "const": "string_pos_neg_output", + "default": "string_pos_neg_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "positive_string", "negative_string", "type", "type"], + "title": "StringPosNegOutput", + "type": "object" + }, + "StringReplaceInvocation": { + "category": "strings", + "class": "invocation", + "classification": "stable", + "description": "Replaces the search string with the replace string", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "string": { + "default": "", + "description": "String to work on", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "String", + "type": "string", + "ui_component": "textarea" + }, + "search_string": { + "default": "", + "description": "String to search for", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Search String", + "type": "string", + "ui_component": "textarea" + }, + "replace_string": { + "default": "", + "description": "String to replace the search", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Replace String", + "type": "string", + "ui_component": "textarea" + }, + "use_regex": { + "default": false, + "description": "Use search string as a regex expression (non regex is case insensitive)", + "field_kind": "input", + "input": "any", + "orig_default": false, + "orig_required": false, + "title": "Use Regex", + "type": "boolean" + }, + "type": { + "const": "string_replace", + "default": "string_replace", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["string", "replace", "regex"], + "title": "String Replace", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/StringOutput" + } + }, + "StringSplitInvocation": { + "category": "strings", + "class": "invocation", + "classification": "stable", + "description": "Splits string into two strings, based on the first occurance of the delimiter. The delimiter will be removed from the string", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "string": { + "default": "", + "description": "String to split", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "String", + "type": "string", + "ui_component": "textarea" + }, + "delimiter": { + "default": "", + "description": "Delimiter to spilt with. blank will split on the first whitespace", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Delimiter", + "type": "string" + }, + "type": { + "const": "string_split", + "default": "string_split", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["string", "split"], + "title": "String Split", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/String2Output" + } + }, + "StringSplitNegInvocation": { + "category": "strings", + "class": "invocation", + "classification": "stable", + "description": "Splits string into two strings, inside [] goes into negative string everthing else goes into positive string. Each [ and ] character is replaced with a space", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "string": { + "default": "", + "description": "String to split", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "String", + "type": "string", + "ui_component": "textarea" + }, + "type": { + "const": "string_split_neg", + "default": "string_split_neg", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["string", "split", "negative"], + "title": "String Split Negative", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/StringPosNegOutput" + } + }, + "StylePresetField": { + "description": "A style preset primitive field", + "properties": { + "style_preset_id": { + "description": "The id of the style preset", + "title": "Style Preset Id", + "type": "string" + } + }, + "required": ["style_preset_id"], + "title": "StylePresetField", + "type": "object" + }, + "StylePresetRecordWithImage": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the style preset." + }, + "preset_data": { + "$ref": "#/components/schemas/PresetData", + "description": "The preset data" + }, + "type": { + "$ref": "#/components/schemas/PresetType", + "description": "The type of style preset" + }, + "is_public": { + "type": "boolean", + "title": "Is Public", + "description": "Whether the preset is visible to other users.", + "default": false + }, + "id": { + "type": "string", + "title": "Id", + "description": "The style preset ID." + }, + "user_id": { + "type": "string", + "title": "User Id", + "description": "The user who owns this style preset." + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Image", + "description": "The path for image" + } + }, + "type": "object", + "required": ["name", "preset_data", "type", "id", "user_id", "image"], + "title": "StylePresetRecordWithImage" + }, + "SubModelType": { + "type": "string", + "enum": [ + "unet", + "transformer", + "text_encoder", + "text_encoder_2", + "text_encoder_3", + "tokenizer", + "tokenizer_2", + "tokenizer_3", + "vae", + "vae_decoder", + "vae_encoder", + "scheduler", + "safety_checker" + ], + "title": "SubModelType", + "description": "Submodel type." + }, + "SubmodelDefinition": { + "properties": { + "path_or_prefix": { + "type": "string", + "title": "Path Or Prefix" + }, + "model_type": { + "$ref": "#/components/schemas/ModelType" + }, + "variant": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelVariantType" + }, + { + "$ref": "#/components/schemas/ClipVariantType" + }, + { + "$ref": "#/components/schemas/FluxVariantType" + }, + { + "$ref": "#/components/schemas/Flux2VariantType" + }, + { + "$ref": "#/components/schemas/ZImageVariantType" + }, + { + "$ref": "#/components/schemas/QwenImageVariantType" + }, + { + "$ref": "#/components/schemas/Qwen3VariantType" + }, + { + "type": "null" + } + ], + "title": "Variant" + } + }, + "type": "object", + "required": ["path_or_prefix", "model_type"], + "title": "SubmodelDefinition" + }, + "SubtractInvocation": { + "category": "math", + "class": "invocation", + "classification": "stable", + "description": "Subtracts two numbers", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "a": { + "default": 0, + "description": "The first number", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "A", + "type": "integer" + }, + "b": { + "default": 0, + "description": "The second number", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "B", + "type": "integer" + }, + "type": { + "const": "sub", + "default": "sub", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["math", "subtract"], + "title": "Subtract Integers", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/IntegerOutput" + } + }, + "T2IAdapterField": { + "properties": { + "image": { + "$ref": "#/components/schemas/ImageField", + "description": "The T2I-Adapter image prompt." + }, + "t2i_adapter_model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "The T2I-Adapter model to use." + }, + "weight": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 1, + "description": "The weight given to the T2I-Adapter", + "title": "Weight" + }, + "begin_step_percent": { + "default": 0, + "description": "When the T2I-Adapter is first applied (% of total steps)", + "maximum": 1, + "minimum": 0, + "title": "Begin Step Percent", + "type": "number" + }, + "end_step_percent": { + "default": 1, + "description": "When the T2I-Adapter is last applied (% of total steps)", + "maximum": 1, + "minimum": 0, + "title": "End Step Percent", + "type": "number" + }, + "resize_mode": { + "default": "just_resize", + "description": "The resize mode to use", + "enum": ["just_resize", "crop_resize", "fill_resize", "just_resize_simple"], + "title": "Resize Mode", + "type": "string" + } + }, + "required": ["image", "t2i_adapter_model"], + "title": "T2IAdapterField", + "type": "object" + }, + "T2IAdapterInvocation": { + "category": "conditioning", + "class": "invocation", + "classification": "stable", + "description": "Collects T2I-Adapter info to pass to other nodes.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The IP-Adapter image prompt.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "t2i_adapter_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The T2I-Adapter model.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "T2I-Adapter Model", + "ui_model_base": ["sd-1", "sdxl"], + "ui_model_type": ["t2i_adapter"], + "ui_order": -1 + }, + "weight": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 1, + "description": "The weight given to the T2I-Adapter", + "field_kind": "input", + "ge": 0, + "input": "any", + "orig_default": 1, + "orig_required": false, + "title": "Weight" + }, + "begin_step_percent": { + "default": 0, + "description": "When the T2I-Adapter is first applied (% of total steps)", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Begin Step Percent", + "type": "number" + }, + "end_step_percent": { + "default": 1, + "description": "When the T2I-Adapter is last applied (% of total steps)", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1, + "orig_required": false, + "title": "End Step Percent", + "type": "number" + }, + "resize_mode": { + "default": "just_resize", + "description": "The resize mode applied to the T2I-Adapter input image so that it matches the target output size.", + "enum": ["just_resize", "crop_resize", "fill_resize", "just_resize_simple"], + "field_kind": "input", + "input": "any", + "orig_default": "just_resize", + "orig_required": false, + "title": "Resize Mode", + "type": "string" + }, + "type": { + "const": "t2i_adapter", + "default": "t2i_adapter", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["t2i_adapter", "control"], + "title": "T2I-Adapter - SD1.5, SDXL", + "type": "object", + "version": "1.0.4", + "output": { + "$ref": "#/components/schemas/T2IAdapterOutput" + } + }, + "T2IAdapterMetadataField": { + "properties": { + "image": { + "$ref": "#/components/schemas/ImageField", + "description": "The control image." + }, + "processed_image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The control image, after processing." + }, + "t2i_adapter_model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "The T2I-Adapter model to use." + }, + "weight": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 1, + "description": "The weight given to the T2I-Adapter", + "title": "Weight" + }, + "begin_step_percent": { + "default": 0, + "description": "When the T2I-Adapter is first applied (% of total steps)", + "maximum": 1, + "minimum": 0, + "title": "Begin Step Percent", + "type": "number" + }, + "end_step_percent": { + "default": 1, + "description": "When the T2I-Adapter is last applied (% of total steps)", + "maximum": 1, + "minimum": 0, + "title": "End Step Percent", + "type": "number" + }, + "resize_mode": { + "default": "just_resize", + "description": "The resize mode to use", + "enum": ["just_resize", "crop_resize", "fill_resize", "just_resize_simple"], + "title": "Resize Mode", + "type": "string" + } + }, + "required": ["image", "t2i_adapter_model"], + "title": "T2IAdapterMetadataField", + "type": "object" + }, + "T2IAdapterOutput": { + "class": "output", + "properties": { + "t2i_adapter": { + "$ref": "#/components/schemas/T2IAdapterField", + "description": "T2I-Adapter(s) to apply", + "field_kind": "output", + "title": "T2I Adapter", + "ui_hidden": false + }, + "type": { + "const": "t2i_adapter_output", + "default": "t2i_adapter_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "t2i_adapter", "type", "type"], + "title": "T2IAdapterOutput", + "type": "object" + }, + "T2IAdapter_Diffusers_SD1_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "type": { + "type": "string", + "const": "t2i_adapter", + "title": "Type", + "default": "t2i_adapter" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlAdapterDefaultSettings" + }, + { + "type": "null" + } + ] + }, + "base": { + "type": "string", + "const": "sd-1", + "title": "Base", + "default": "sd-1" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "format", + "repo_variant", + "type", + "default_settings", + "base" + ], + "title": "T2IAdapter_Diffusers_SD1_Config" + }, + "T2IAdapter_Diffusers_SDXL_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "type": { + "type": "string", + "const": "t2i_adapter", + "title": "Type", + "default": "t2i_adapter" + }, + "default_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlAdapterDefaultSettings" + }, + { + "type": "null" + } + ] + }, + "base": { + "type": "string", + "const": "sdxl", + "title": "Base", + "default": "sdxl" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "format", + "repo_variant", + "type", + "default_settings", + "base" + ], + "title": "T2IAdapter_Diffusers_SDXL_Config" + }, + "T5EncoderField": { + "properties": { + "tokenizer": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Info to load tokenizer submodel" + }, + "text_encoder": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Info to load text_encoder submodel" + }, + "loras": { + "description": "LoRAs to apply on model loading", + "items": { + "$ref": "#/components/schemas/LoRAField" + }, + "title": "Loras", + "type": "array" + } + }, + "required": ["tokenizer", "text_encoder", "loras"], + "title": "T5EncoderField", + "type": "object" + }, + "T5Encoder_BnBLLMint8_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "base": { + "type": "string", + "const": "any", + "title": "Base", + "default": "any" + }, + "type": { + "type": "string", + "const": "t5_encoder", + "title": "Type", + "default": "t5_encoder" + }, + "format": { + "type": "string", + "const": "bnb_quantized_int8b", + "title": "Format", + "default": "bnb_quantized_int8b" + }, + "cpu_only": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Cpu Only", + "description": "Whether this model should run on CPU only" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "base", + "type", + "format", + "cpu_only" + ], + "title": "T5Encoder_BnBLLMint8_Config", + "description": "Configuration for T5 Encoder models quantized by bitsandbytes' LLM.int8." + }, + "T5Encoder_T5Encoder_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "base": { + "type": "string", + "const": "any", + "title": "Base", + "default": "any" + }, + "type": { + "type": "string", + "const": "t5_encoder", + "title": "Type", + "default": "t5_encoder" + }, + "format": { + "type": "string", + "const": "t5_encoder", + "title": "Format", + "default": "t5_encoder" + }, + "cpu_only": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Cpu Only", + "description": "Whether this model should run on CPU only" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "base", + "type", + "format", + "cpu_only" + ], + "title": "T5Encoder_T5Encoder_Config", + "description": "Configuration for T5 Encoder models in a bespoke, diffusers-like format. The model weights are expected to be in\na folder called text_encoder_2 inside the model directory, with a config file named model.safetensors.index.json." + }, + "TBLR": { + "properties": { + "top": { + "title": "Top", + "type": "integer" + }, + "bottom": { + "title": "Bottom", + "type": "integer" + }, + "left": { + "title": "Left", + "type": "integer" + }, + "right": { + "title": "Right", + "type": "integer" + } + }, + "required": ["top", "bottom", "left", "right"], + "title": "TBLR", + "type": "object" + }, + "TI_File_SD1_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "embedding", + "title": "Type", + "default": "embedding" + }, + "format": { + "type": "string", + "const": "embedding_file", + "title": "Format", + "default": "embedding_file" + }, + "base": { + "type": "string", + "const": "sd-1", + "title": "Base", + "default": "sd-1" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "format", + "base" + ], + "title": "TI_File_SD1_Config" + }, + "TI_File_SD2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "embedding", + "title": "Type", + "default": "embedding" + }, + "format": { + "type": "string", + "const": "embedding_file", + "title": "Format", + "default": "embedding_file" + }, + "base": { + "type": "string", + "const": "sd-2", + "title": "Base", + "default": "sd-2" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "format", + "base" + ], + "title": "TI_File_SD2_Config" + }, + "TI_File_SDXL_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "embedding", + "title": "Type", + "default": "embedding" + }, + "format": { + "type": "string", + "const": "embedding_file", + "title": "Format", + "default": "embedding_file" + }, + "base": { + "type": "string", + "const": "sdxl", + "title": "Base", + "default": "sdxl" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "format", + "base" + ], + "title": "TI_File_SDXL_Config" + }, + "TI_Folder_SD1_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "embedding", + "title": "Type", + "default": "embedding" + }, + "format": { + "type": "string", + "const": "embedding_folder", + "title": "Format", + "default": "embedding_folder" + }, + "base": { + "type": "string", + "const": "sd-1", + "title": "Base", + "default": "sd-1" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "format", + "base" + ], + "title": "TI_Folder_SD1_Config" + }, + "TI_Folder_SD2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "embedding", + "title": "Type", + "default": "embedding" + }, + "format": { + "type": "string", + "const": "embedding_folder", + "title": "Format", + "default": "embedding_folder" + }, + "base": { + "type": "string", + "const": "sd-2", + "title": "Base", + "default": "sd-2" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "format", + "base" + ], + "title": "TI_Folder_SD2_Config" + }, + "TI_Folder_SDXL_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "type": { + "type": "string", + "const": "embedding", + "title": "Type", + "default": "embedding" + }, + "format": { + "type": "string", + "const": "embedding_folder", + "title": "Format", + "default": "embedding_folder" + }, + "base": { + "type": "string", + "const": "sdxl", + "title": "Base", + "default": "sdxl" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "type", + "format", + "base" + ], + "title": "TI_Folder_SDXL_Config" + }, + "TensorField": { + "description": "A tensor primitive field.", + "properties": { + "tensor_name": { + "description": "The name of a tensor.", + "title": "Tensor Name", + "type": "string" + } + }, + "required": ["tensor_name"], + "title": "TensorField", + "type": "object" + }, + "TextLLMInvocation": { + "category": "llm", + "class": "invocation", + "classification": "beta", + "description": "Run a text language model to generate or expand text (e.g. for prompt expansion).", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "prompt": { + "default": "", + "description": "Input text prompt.", + "field_kind": "input", + "input": "any", + "orig_default": "", + "orig_required": false, + "title": "Prompt", + "type": "string", + "ui_component": "textarea" + }, + "system_prompt": { + "default": "You are an expert prompt writer for AI image generation. Given a brief description, expand it into a detailed, vivid prompt suitable for generating high-quality images. Only output the expanded prompt, nothing else.", + "description": "System prompt that guides the model's behavior.", + "field_kind": "input", + "input": "any", + "orig_default": "You are an expert prompt writer for AI image generation. Given a brief description, expand it into a detailed, vivid prompt suitable for generating high-quality images. Only output the expanded prompt, nothing else.", + "orig_required": false, + "title": "System Prompt", + "type": "string", + "ui_component": "textarea" + }, + "text_llm_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The text language model to use for text generation", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Text LLM Model", + "ui_model_type": ["text_llm"] + }, + "max_tokens": { + "default": 300, + "description": "Maximum number of tokens to generate.", + "field_kind": "input", + "input": "any", + "maximum": 2048, + "minimum": 1, + "orig_default": 300, + "orig_required": false, + "title": "Max Tokens", + "type": "integer" + }, + "type": { + "const": "text_llm", + "default": "text_llm", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["llm", "text", "prompt"], + "title": "Text LLM", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/StringOutput" + } + }, + "TextLLM_Diffusers_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "type": { + "type": "string", + "const": "text_llm", + "title": "Type", + "default": "text_llm" + }, + "base": { + "type": "string", + "const": "any", + "title": "Base", + "default": "any" + }, + "cpu_only": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Cpu Only", + "description": "Whether this model should run on CPU only" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "format", + "repo_variant", + "type", + "base", + "cpu_only" + ], + "title": "TextLLM_Diffusers_Config", + "description": "Model config for text-only causal language models (e.g. Llama, Phi, Qwen, Mistral)." + }, + "Tile": { + "properties": { + "coords": { + "$ref": "#/components/schemas/TBLR", + "description": "The coordinates of this tile relative to its parent image." + }, + "overlap": { + "$ref": "#/components/schemas/TBLR", + "description": "The amount of overlap with adjacent tiles on each side of this tile." + } + }, + "required": ["coords", "overlap"], + "title": "Tile", + "type": "object" + }, + "TileToPropertiesInvocation": { + "category": "tiles", + "class": "invocation", + "classification": "stable", + "description": "Split a Tile into its individual properties.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "tile": { + "anyOf": [ + { + "$ref": "#/components/schemas/Tile" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The tile to split into properties.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "type": { + "const": "tile_to_properties", + "default": "tile_to_properties", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["tiles"], + "title": "Tile to Properties", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/TileToPropertiesOutput" + } + }, + "TileToPropertiesOutput": { + "class": "output", + "properties": { + "coords_left": { + "description": "Left coordinate of the tile relative to its parent image.", + "field_kind": "output", + "title": "Coords Left", + "type": "integer", + "ui_hidden": false + }, + "coords_right": { + "description": "Right coordinate of the tile relative to its parent image.", + "field_kind": "output", + "title": "Coords Right", + "type": "integer", + "ui_hidden": false + }, + "coords_top": { + "description": "Top coordinate of the tile relative to its parent image.", + "field_kind": "output", + "title": "Coords Top", + "type": "integer", + "ui_hidden": false + }, + "coords_bottom": { + "description": "Bottom coordinate of the tile relative to its parent image.", + "field_kind": "output", + "title": "Coords Bottom", + "type": "integer", + "ui_hidden": false + }, + "width": { + "description": "The width of the tile. Equal to coords_right - coords_left.", + "field_kind": "output", + "title": "Width", + "type": "integer", + "ui_hidden": false + }, + "height": { + "description": "The height of the tile. Equal to coords_bottom - coords_top.", + "field_kind": "output", + "title": "Height", + "type": "integer", + "ui_hidden": false + }, + "overlap_top": { + "description": "Overlap between this tile and its top neighbor.", + "field_kind": "output", + "title": "Overlap Top", + "type": "integer", + "ui_hidden": false + }, + "overlap_bottom": { + "description": "Overlap between this tile and its bottom neighbor.", + "field_kind": "output", + "title": "Overlap Bottom", + "type": "integer", + "ui_hidden": false + }, + "overlap_left": { + "description": "Overlap between this tile and its left neighbor.", + "field_kind": "output", + "title": "Overlap Left", + "type": "integer", + "ui_hidden": false + }, + "overlap_right": { + "description": "Overlap between this tile and its right neighbor.", + "field_kind": "output", + "title": "Overlap Right", + "type": "integer", + "ui_hidden": false + }, + "type": { + "const": "tile_to_properties_output", + "default": "tile_to_properties_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": [ + "output_meta", + "coords_left", + "coords_right", + "coords_top", + "coords_bottom", + "width", + "height", + "overlap_top", + "overlap_bottom", + "overlap_left", + "overlap_right", + "type", + "type" + ], + "title": "TileToPropertiesOutput", + "type": "object" + }, + "TileWithImage": { + "properties": { + "tile": { + "$ref": "#/components/schemas/Tile" + }, + "image": { + "$ref": "#/components/schemas/ImageField" + } + }, + "required": ["tile", "image"], + "title": "TileWithImage", + "type": "object" + }, + "TiledMultiDiffusionDenoiseLatents": { + "category": "latents", + "class": "invocation", + "classification": "stable", + "description": "Tiled Multi-Diffusion denoising.\n\nThis node handles automatically tiling the input image, and is primarily intended for global refinement of images\nin tiled upscaling workflows. Future Multi-Diffusion nodes should allow the user to specify custom regions with\ndifferent parameters for each region to harness the full power of Multi-Diffusion.\n\nThis node has a similar interface to the `DenoiseLatents` node, but it has a reduced feature set (no IP-Adapter,\nT2I-Adapter, masking, etc.).", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "positive_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/ConditioningField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Positive conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "negative_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/ConditioningField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Negative conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "noise": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Noise tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "tile_height": { + "default": 1024, + "description": "Height of the tiles in image space.", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "multipleOf": 8, + "orig_default": 1024, + "orig_required": false, + "title": "Tile Height", + "type": "integer" + }, + "tile_width": { + "default": 1024, + "description": "Width of the tiles in image space.", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "multipleOf": 8, + "orig_default": 1024, + "orig_required": false, + "title": "Tile Width", + "type": "integer" + }, + "tile_overlap": { + "default": 32, + "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.", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "multipleOf": 8, + "orig_default": 32, + "orig_required": false, + "title": "Tile Overlap", + "type": "integer" + }, + "steps": { + "default": 18, + "description": "Number of steps to run", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 18, + "orig_required": false, + "title": "Steps", + "type": "integer" + }, + "cfg_scale": { + "anyOf": [ + { + "type": "number" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ], + "default": 6.0, + "description": "Classifier-Free Guidance scale", + "field_kind": "input", + "input": "any", + "orig_default": 6.0, + "orig_required": false, + "title": "CFG Scale" + }, + "denoising_start": { + "default": 0.0, + "description": "When to start denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0.0, + "orig_required": false, + "title": "Denoising Start", + "type": "number" + }, + "denoising_end": { + "default": 1.0, + "description": "When to stop denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Denoising End", + "type": "number" + }, + "scheduler": { + "default": "euler", + "description": "Scheduler to use during inference", + "enum": [ + "ddim", + "ddpm", + "deis", + "deis_k", + "lms", + "lms_k", + "pndm", + "heun", + "heun_k", + "euler", + "euler_k", + "euler_a", + "kdpm_2", + "kdpm_2_k", + "kdpm_2_a", + "kdpm_2_a_k", + "dpmpp_2s", + "dpmpp_2s_k", + "dpmpp_2m", + "dpmpp_2m_k", + "dpmpp_2m_sde", + "dpmpp_2m_sde_k", + "dpmpp_3m", + "dpmpp_3m_k", + "dpmpp_sde", + "dpmpp_sde_k", + "er_sde", + "unipc", + "unipc_k", + "lcm", + "tcd" + ], + "field_kind": "input", + "input": "any", + "orig_default": "euler", + "orig_required": false, + "title": "Scheduler", + "type": "string", + "ui_type": "SchedulerField" + }, + "unet": { + "anyOf": [ + { + "$ref": "#/components/schemas/UNetField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "UNet (scheduler, LoRAs)", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "UNet" + }, + "cfg_rescale_multiplier": { + "default": 0, + "description": "Rescale multiplier for CFG guidance, used for models trained with zero-terminal SNR", + "exclusiveMaximum": 1, + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "CFG Rescale Multiplier", + "type": "number" + }, + "control": { + "anyOf": [ + { + "$ref": "#/components/schemas/ControlField" + }, + { + "items": { + "$ref": "#/components/schemas/ControlField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Control" + }, + "type": { + "const": "tiled_multi_diffusion_denoise_latents", + "default": "tiled_multi_diffusion_denoise_latents", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["upscale", "denoise"], + "title": "Tiled Multi-Diffusion Denoise - SD1.5, SDXL", + "type": "object", + "version": "1.0.1", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "TransformerField": { + "properties": { + "transformer": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Info to load Transformer submodel" + }, + "loras": { + "description": "LoRAs to apply on model loading", + "items": { + "$ref": "#/components/schemas/LoRAField" + }, + "title": "Loras", + "type": "array" + } + }, + "required": ["transformer", "loras"], + "title": "TransformerField", + "type": "object" + }, + "UIComponent": { + "description": "The type of UI component to use for a field, used to override the default components, which are\ninferred from the field type.", + "enum": ["none", "textarea", "slider"], + "title": "UIComponent", + "type": "string" + }, + "UIConfigBase": { + "description": "Provides additional node configuration to the UI.\nThis is used internally by the @invocation decorator logic. Do not use this directly.", + "properties": { + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The node's tags", + "title": "Tags" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The node's display name", + "title": "Title" + }, + "category": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The node's category", + "title": "Category" + }, + "version": { + "description": "The node's version. Should be a valid semver string e.g. \"1.0.0\" or \"3.8.13\".", + "title": "Version", + "type": "string" + }, + "node_pack": { + "description": "The node pack that this node belongs to, will be 'invokeai' for built-in nodes", + "title": "Node Pack", + "type": "string" + }, + "classification": { + "$ref": "#/components/schemas/Classification", + "default": "stable", + "description": "The node's classification" + } + }, + "required": ["tags", "title", "category", "version", "node_pack", "classification"], + "title": "UIConfigBase", + "type": "object" + }, + "UIType": { + "description": "Type hints for the UI for situations in which the field type is not enough to infer the correct UI type.\n\n- Model Fields\nThe most common node-author-facing use will be for model fields. Internally, there is no difference\nbetween SD-1, SD-2 and SDXL model fields - they all use the class `MainModelField`. To ensure the\nbase-model-specific UI is rendered, use e.g. `ui_type=UIType.SDXLMainModelField` to indicate that\nthe field is an SDXL main model field.\n\n- Any Field\nWe cannot infer the usage of `typing.Any` via schema parsing, so you *must* use `ui_type=UIType.Any` to\nindicate that the field accepts any type. Use with caution. This cannot be used on outputs.\n\n- Scheduler Field\nSpecial handling in the UI is needed for this field, which otherwise would be parsed as a plain enum field.\n\n- Internal Fields\nSimilar to the Any Field, the `collect` and `iterate` nodes make use of `typing.Any`. To facilitate\nhandling these types in the client, we use `UIType._Collection` and `UIType._CollectionItem`. These\nshould not be used by node authors.\n\n- DEPRECATED Fields\nThese types are deprecated and should not be used by node authors. A warning will be logged if one is\nused, and the type will be ignored. They are included here for backwards compatibility.", + "enum": [ + "SchedulerField", + "AnyField", + "CollectionField", + "CollectionItemField", + "IsIntermediate", + "DEPRECATED_Boolean", + "DEPRECATED_Color", + "DEPRECATED_Conditioning", + "DEPRECATED_Control", + "DEPRECATED_Float", + "DEPRECATED_Image", + "DEPRECATED_Integer", + "DEPRECATED_Latents", + "DEPRECATED_String", + "DEPRECATED_BooleanCollection", + "DEPRECATED_ColorCollection", + "DEPRECATED_ConditioningCollection", + "DEPRECATED_ControlCollection", + "DEPRECATED_FloatCollection", + "DEPRECATED_ImageCollection", + "DEPRECATED_IntegerCollection", + "DEPRECATED_LatentsCollection", + "DEPRECATED_StringCollection", + "DEPRECATED_BooleanPolymorphic", + "DEPRECATED_ColorPolymorphic", + "DEPRECATED_ConditioningPolymorphic", + "DEPRECATED_ControlPolymorphic", + "DEPRECATED_FloatPolymorphic", + "DEPRECATED_ImagePolymorphic", + "DEPRECATED_IntegerPolymorphic", + "DEPRECATED_LatentsPolymorphic", + "DEPRECATED_StringPolymorphic", + "DEPRECATED_UNet", + "DEPRECATED_Vae", + "DEPRECATED_CLIP", + "DEPRECATED_Collection", + "DEPRECATED_CollectionItem", + "DEPRECATED_Enum", + "DEPRECATED_WorkflowField", + "DEPRECATED_BoardField", + "DEPRECATED_MetadataItem", + "DEPRECATED_MetadataItemCollection", + "DEPRECATED_MetadataItemPolymorphic", + "DEPRECATED_MetadataDict", + "DEPRECATED_MainModelField", + "DEPRECATED_CogView4MainModelField", + "DEPRECATED_FluxMainModelField", + "DEPRECATED_SD3MainModelField", + "DEPRECATED_SDXLMainModelField", + "DEPRECATED_SDXLRefinerModelField", + "DEPRECATED_ONNXModelField", + "DEPRECATED_VAEModelField", + "DEPRECATED_FluxVAEModelField", + "DEPRECATED_LoRAModelField", + "DEPRECATED_ControlNetModelField", + "DEPRECATED_IPAdapterModelField", + "DEPRECATED_T2IAdapterModelField", + "DEPRECATED_T5EncoderModelField", + "DEPRECATED_CLIPEmbedModelField", + "DEPRECATED_CLIPLEmbedModelField", + "DEPRECATED_CLIPGEmbedModelField", + "DEPRECATED_SpandrelImageToImageModelField", + "DEPRECATED_ControlLoRAModelField", + "DEPRECATED_SigLipModelField", + "DEPRECATED_FluxReduxModelField", + "DEPRECATED_LLaVAModelField", + "DEPRECATED_Imagen3ModelField", + "DEPRECATED_Imagen4ModelField", + "DEPRECATED_ChatGPT4oModelField", + "DEPRECATED_Gemini2_5ModelField", + "DEPRECATED_FluxKontextModelField", + "DEPRECATED_Veo3ModelField", + "DEPRECATED_RunwayModelField" + ], + "title": "UIType", + "type": "string" + }, + "UNetField": { + "properties": { + "unet": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Info to load unet submodel" + }, + "scheduler": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Info to load scheduler submodel" + }, + "loras": { + "description": "LoRAs to apply on model loading", + "items": { + "$ref": "#/components/schemas/LoRAField" + }, + "title": "Loras", + "type": "array" + }, + "seamless_axes": { + "description": "Axes(\"x\" and \"y\") to which apply seamless", + "items": { + "type": "string" + }, + "title": "Seamless Axes", + "type": "array" + }, + "freeu_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/FreeUConfig" + }, + { + "type": "null" + } + ], + "default": null, + "description": "FreeU configuration" + } + }, + "required": ["unet", "scheduler", "loras"], + "title": "UNetField", + "type": "object" + }, + "UNetOutput": { + "class": "output", + "description": "Base class for invocations that output a UNet field.", + "properties": { + "unet": { + "$ref": "#/components/schemas/UNetField", + "description": "UNet (scheduler, LoRAs)", + "field_kind": "output", + "title": "UNet", + "ui_hidden": false + }, + "type": { + "const": "unet_output", + "default": "unet_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "unet", "type", "type"], + "title": "UNetOutput", + "type": "object" + }, + "URLModelSource": { + "properties": { + "url": { + "type": "string", + "minLength": 1, + "format": "uri", + "title": "Url" + }, + "access_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Access Token" + }, + "type": { + "type": "string", + "const": "url", + "title": "Type", + "default": "url" + } + }, + "type": "object", + "required": ["url"], + "title": "URLModelSource", + "description": "A generic URL point to a checkpoint file." + }, + "URLRegexTokenPair": { + "properties": { + "url_regex": { + "type": "string", + "title": "Url Regex", + "description": "Regular expression to match against the URL" + }, + "token": { + "type": "string", + "title": "Token", + "description": "Token to use when the URL matches the regex" + } + }, + "type": "object", + "required": ["url_regex", "token"], + "title": "URLRegexTokenPair" + }, + "UninstallNodePackResponse": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the uninstalled node pack." + }, + "success": { + "type": "boolean", + "title": "Success", + "description": "Whether the uninstall was successful." + }, + "message": { + "type": "string", + "title": "Message", + "description": "Status message." + } + }, + "type": "object", + "required": ["name", "success", "message"], + "title": "UninstallNodePackResponse", + "description": "Response after uninstalling a node pack." + }, + "Unknown_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "base": { + "type": "string", + "const": "unknown", + "title": "Base", + "default": "unknown" + }, + "type": { + "type": "string", + "const": "unknown", + "title": "Type", + "default": "unknown" + }, + "format": { + "type": "string", + "const": "unknown", + "title": "Format", + "default": "unknown" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "base", + "type", + "format" + ], + "title": "Unknown_Config", + "description": "Model config for unknown models, used as a fallback when we cannot positively identify a model." + }, + "UnsharpMaskInvocation": { + "category": "image", + "class": "invocation", + "classification": "stable", + "description": "Applies an unsharp mask filter to an image", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to use", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "radius": { + "default": 2, + "description": "Unsharp mask radius", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 2, + "orig_required": false, + "title": "Radius", + "type": "number" + }, + "strength": { + "default": 50, + "description": "Unsharp mask strength", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 50, + "orig_required": false, + "title": "Strength", + "type": "number" + }, + "type": { + "const": "unsharp_mask", + "default": "unsharp_mask", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "unsharp_mask"], + "title": "Unsharp Mask", + "type": "object", + "version": "1.2.2", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "UnstarredImagesResult": { + "properties": { + "affected_boards": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Affected Boards", + "description": "The ids of boards affected by the delete operation" + }, + "unstarred_images": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Unstarred Images", + "description": "The names of the images that were unstarred" + } + }, + "type": "object", + "required": ["affected_boards", "unstarred_images"], + "title": "UnstarredImagesResult" + }, + "UpdateAppGenerationSettingsRequest": { + "properties": { + "image_subfolder_strategy": { + "type": "string", + "enum": ["flat", "date", "type", "hash"], + "title": "Image Subfolder Strategy", + "description": "Strategy for organizing images into subfolders." + }, + "max_queue_history": { + "anyOf": [ + { + "type": "integer", + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Max Queue History", + "description": "Keep the last N completed, failed, and canceled queue items on startup. Set to 0 to prune all terminal items." + } + }, + "type": "object", + "title": "UpdateAppGenerationSettingsRequest", + "description": "Writable generation-related app settings." + }, + "UserDTO": { + "properties": { + "user_id": { + "type": "string", + "title": "User Id", + "description": "Unique user identifier" + }, + "email": { + "type": "string", + "title": "Email", + "description": "User email address" + }, + "display_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Display Name", + "description": "Display name" + }, + "is_admin": { + "type": "boolean", + "title": "Is Admin", + "description": "Whether user has admin privileges", + "default": false + }, + "is_active": { + "type": "boolean", + "title": "Is Active", + "description": "Whether user account is active", + "default": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "When the user was created" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At", + "description": "When the user was last updated" + }, + "last_login_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Login At", + "description": "When user last logged in" + } + }, + "type": "object", + "required": ["user_id", "email", "created_at", "updated_at"], + "title": "UserDTO", + "description": "User data transfer object." + }, + "UserProfileUpdateRequest": { + "properties": { + "display_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Display Name", + "description": "New display name" + }, + "current_password": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Current Password", + "description": "Current password (required when changing password)" + }, + "new_password": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "New Password", + "description": "New password" + } + }, + "type": "object", + "title": "UserProfileUpdateRequest", + "description": "Request body for a user to update their own profile." + }, + "VAEField": { + "properties": { + "vae": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Info to load vae submodel" + }, + "seamless_axes": { + "description": "Axes(\"x\" and \"y\") to which apply seamless", + "items": { + "type": "string" + }, + "title": "Seamless Axes", + "type": "array" + } + }, + "required": ["vae"], + "title": "VAEField", + "type": "object" + }, + "VAELoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Loads a VAE model, outputting a VaeLoaderOutput", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "vae_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE model to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "VAE", + "ui_model_base": ["sd-1", "sd-2", "sdxl", "sd-3", "flux", "flux2"], + "ui_model_type": ["vae"] + }, + "type": { + "const": "vae_loader", + "default": "vae_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["vae", "model"], + "title": "VAE Model - SD1.5, SD2, SDXL, SD3, FLUX", + "type": "object", + "version": "1.0.4", + "output": { + "$ref": "#/components/schemas/VAEOutput" + } + }, + "VAEOutput": { + "class": "output", + "description": "Base class for invocations that output a VAE field", + "properties": { + "vae": { + "$ref": "#/components/schemas/VAEField", + "description": "VAE", + "field_kind": "output", + "title": "VAE", + "ui_hidden": false + }, + "type": { + "const": "vae_output", + "default": "vae_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "vae", "type", "type"], + "title": "VAEOutput", + "type": "object" + }, + "VAE_Checkpoint_Anima_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "type": { + "type": "string", + "const": "vae", + "title": "Type", + "default": "vae" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "base": { + "type": "string", + "const": "anima", + "title": "Base", + "default": "anima" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "config_path", + "type", + "format", + "base" + ], + "title": "VAE_Checkpoint_Anima_Config", + "description": "Model config for Anima QwenImage VAE checkpoint models (AutoencoderKLQwenImage)." + }, + "VAE_Checkpoint_FLUX_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "type": { + "type": "string", + "const": "vae", + "title": "Type", + "default": "vae" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "base": { + "type": "string", + "const": "flux", + "title": "Base", + "default": "flux" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "config_path", + "type", + "format", + "base" + ], + "title": "VAE_Checkpoint_FLUX_Config" + }, + "VAE_Checkpoint_Flux2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "type": { + "type": "string", + "const": "vae", + "title": "Type", + "default": "vae" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "base": { + "type": "string", + "const": "flux2", + "title": "Base", + "default": "flux2" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "config_path", + "type", + "format", + "base" + ], + "title": "VAE_Checkpoint_Flux2_Config", + "description": "Model config for FLUX.2 VAE checkpoint models (AutoencoderKLFlux2)." + }, + "VAE_Checkpoint_QwenImage_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "type": { + "type": "string", + "const": "vae", + "title": "Type", + "default": "vae" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "base": { + "type": "string", + "const": "qwen-image", + "title": "Base", + "default": "qwen-image" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "config_path", + "type", + "format", + "base" + ], + "title": "VAE_Checkpoint_QwenImage_Config", + "description": "Model config for Qwen Image VAE checkpoint models (AutoencoderKLQwenImage)." + }, + "VAE_Checkpoint_SD1_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "type": { + "type": "string", + "const": "vae", + "title": "Type", + "default": "vae" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "base": { + "type": "string", + "const": "sd-1", + "title": "Base", + "default": "sd-1" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "config_path", + "type", + "format", + "base" + ], + "title": "VAE_Checkpoint_SD1_Config" + }, + "VAE_Checkpoint_SD2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "type": { + "type": "string", + "const": "vae", + "title": "Type", + "default": "vae" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "base": { + "type": "string", + "const": "sd-2", + "title": "Base", + "default": "sd-2" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "config_path", + "type", + "format", + "base" + ], + "title": "VAE_Checkpoint_SD2_Config" + }, + "VAE_Checkpoint_SDXL_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "config_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Config Path", + "description": "Path to the config for this model, if any." + }, + "type": { + "type": "string", + "const": "vae", + "title": "Type", + "default": "vae" + }, + "format": { + "type": "string", + "const": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "base": { + "type": "string", + "const": "sdxl", + "title": "Base", + "default": "sdxl" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "config_path", + "type", + "format", + "base" + ], + "title": "VAE_Checkpoint_SDXL_Config" + }, + "VAE_Diffusers_Flux2_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "type": { + "type": "string", + "const": "vae", + "title": "Type", + "default": "vae" + }, + "base": { + "type": "string", + "const": "flux2", + "title": "Base", + "default": "flux2" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "format", + "repo_variant", + "type", + "base" + ], + "title": "VAE_Diffusers_Flux2_Config", + "description": "Model config for FLUX.2 VAE models in diffusers format (AutoencoderKLFlux2)." + }, + "VAE_Diffusers_SD1_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "type": { + "type": "string", + "const": "vae", + "title": "Type", + "default": "vae" + }, + "base": { + "type": "string", + "const": "sd-1", + "title": "Base", + "default": "sd-1" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "format", + "repo_variant", + "type", + "base" + ], + "title": "VAE_Diffusers_SD1_Config" + }, + "VAE_Diffusers_SDXL_Config": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "A unique key for this model." + }, + "hash": { + "type": "string", + "title": "Hash", + "description": "The hash of the model file(s)." + }, + "path": { + "type": "string", + "title": "Path", + "description": "Path to the model on the filesystem. Relative paths are relative to the Invoke root directory." + }, + "file_size": { + "type": "integer", + "title": "File Size", + "description": "The size of the model in bytes." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the model." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Model description" + }, + "source": { + "type": "string", + "title": "Source", + "description": "The original source of the model (path, URL or repo_id)." + }, + "source_type": { + "$ref": "#/components/schemas/ModelSourceType", + "description": "The type of source" + }, + "source_api_response": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Api Response", + "description": "The original API response from the source, as stringified JSON." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Optional URL for the model (e.g. download page or model page)." + }, + "cover_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image", + "description": "Url for image to preview model" + }, + "format": { + "type": "string", + "const": "diffusers", + "title": "Format", + "default": "diffusers" + }, + "repo_variant": { + "$ref": "#/components/schemas/ModelRepoVariant", + "default": "" + }, + "type": { + "type": "string", + "const": "vae", + "title": "Type", + "default": "vae" + }, + "base": { + "type": "string", + "const": "sdxl", + "title": "Base", + "default": "sdxl" + } + }, + "type": "object", + "required": [ + "key", + "hash", + "path", + "file_size", + "name", + "description", + "source", + "source_type", + "source_api_response", + "source_url", + "cover_image", + "format", + "repo_variant", + "type", + "base" + ], + "title": "VAE_Diffusers_SDXL_Config" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError" + }, + "VirtualSubBoardDTO": { + "properties": { + "virtual_board_id": { + "type": "string", + "title": "Virtual Board Id", + "description": "The virtual board ID, e.g. 'by_date:2026-03-18'." + }, + "board_name": { + "type": "string", + "title": "Board Name", + "description": "The display name of the virtual sub-board, e.g. '2026-03-18'." + }, + "date": { + "type": "string", + "title": "Date", + "description": "The ISO date string, e.g. '2026-03-18'." + }, + "image_count": { + "type": "integer", + "title": "Image Count", + "description": "The number of general images for this date." + }, + "asset_count": { + "type": "integer", + "title": "Asset Count", + "description": "The number of asset images for this date." + }, + "cover_image_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cover Image Name", + "description": "The most recent image name for this date." + } + }, + "type": "object", + "required": ["virtual_board_id", "board_name", "date", "image_count", "asset_count"], + "title": "VirtualSubBoardDTO", + "description": "A virtual sub-board computed from image metadata, not stored in the database." + }, + "Workflow": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the workflow." + }, + "author": { + "type": "string", + "title": "Author", + "description": "The author of the workflow." + }, + "description": { + "type": "string", + "title": "Description", + "description": "The description of the workflow." + }, + "version": { + "type": "string", + "title": "Version", + "description": "The version of the workflow." + }, + "contact": { + "type": "string", + "title": "Contact", + "description": "The contact of the workflow." + }, + "tags": { + "type": "string", + "title": "Tags", + "description": "The tags of the workflow." + }, + "notes": { + "type": "string", + "title": "Notes", + "description": "The notes of the workflow." + }, + "exposedFields": { + "items": { + "$ref": "#/components/schemas/ExposedField" + }, + "type": "array", + "title": "Exposedfields", + "description": "The exposed fields of the workflow." + }, + "meta": { + "$ref": "#/components/schemas/WorkflowMeta", + "description": "The meta of the workflow." + }, + "nodes": { + "items": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + "type": "array", + "title": "Nodes", + "description": "The nodes of the workflow." + }, + "edges": { + "items": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + "type": "array", + "title": "Edges", + "description": "The edges of the workflow." + }, + "form": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Form", + "description": "The form of the workflow." + }, + "id": { + "type": "string", + "title": "Id", + "description": "The id of the workflow." + } + }, + "type": "object", + "required": [ + "name", + "author", + "description", + "version", + "contact", + "tags", + "notes", + "exposedFields", + "meta", + "nodes", + "edges", + "id" + ], + "title": "Workflow" + }, + "WorkflowAndGraphResponse": { + "properties": { + "workflow": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Workflow", + "description": "The workflow used to generate the image, as stringified JSON" + }, + "graph": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Graph", + "description": "The graph used to generate the image, as stringified JSON" + } + }, + "type": "object", + "required": ["workflow", "graph"], + "title": "WorkflowAndGraphResponse" + }, + "WorkflowCategory": { + "type": "string", + "enum": ["user", "default"], + "title": "WorkflowCategory" + }, + "WorkflowMeta": { + "properties": { + "version": { + "type": "string", + "title": "Version", + "description": "The version of the workflow schema." + }, + "category": { + "$ref": "#/components/schemas/WorkflowCategory", + "description": "The category of the workflow (user or default)." + } + }, + "type": "object", + "required": ["version", "category"], + "title": "WorkflowMeta" + }, + "WorkflowRecordDTO": { + "properties": { + "workflow_id": { + "type": "string", + "title": "Workflow Id", + "description": "The id of the workflow." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the workflow." + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + } + ], + "title": "Created At", + "description": "The created timestamp of the workflow." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + } + ], + "title": "Updated At", + "description": "The updated timestamp of the workflow." + }, + "opened_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Opened At", + "description": "The opened timestamp of the workflow." + }, + "user_id": { + "type": "string", + "title": "User Id", + "description": "The id of the user who owns this workflow." + }, + "is_public": { + "type": "boolean", + "title": "Is Public", + "description": "Whether this workflow is shared with all users." + }, + "workflow": { + "$ref": "#/components/schemas/Workflow", + "description": "The workflow." + } + }, + "type": "object", + "required": ["workflow_id", "name", "created_at", "updated_at", "user_id", "is_public", "workflow"], + "title": "WorkflowRecordDTO" + }, + "WorkflowRecordListItemWithThumbnailDTO": { + "properties": { + "workflow_id": { + "type": "string", + "title": "Workflow Id", + "description": "The id of the workflow." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the workflow." + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + } + ], + "title": "Created At", + "description": "The created timestamp of the workflow." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + } + ], + "title": "Updated At", + "description": "The updated timestamp of the workflow." + }, + "opened_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Opened At", + "description": "The opened timestamp of the workflow." + }, + "user_id": { + "type": "string", + "title": "User Id", + "description": "The id of the user who owns this workflow." + }, + "is_public": { + "type": "boolean", + "title": "Is Public", + "description": "Whether this workflow is shared with all users." + }, + "description": { + "type": "string", + "title": "Description", + "description": "The description of the workflow." + }, + "category": { + "$ref": "#/components/schemas/WorkflowCategory", + "description": "The description of the workflow." + }, + "tags": { + "type": "string", + "title": "Tags", + "description": "The tags of the workflow." + }, + "thumbnail_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Thumbnail Url", + "description": "The URL of the workflow thumbnail." + } + }, + "type": "object", + "required": [ + "workflow_id", + "name", + "created_at", + "updated_at", + "user_id", + "is_public", + "description", + "category", + "tags" + ], + "title": "WorkflowRecordListItemWithThumbnailDTO" + }, + "WorkflowRecordOrderBy": { + "type": "string", + "enum": ["created_at", "updated_at", "opened_at", "name", "is_public"], + "title": "WorkflowRecordOrderBy", + "description": "The order by options for workflow records" + }, + "WorkflowRecordWithThumbnailDTO": { + "properties": { + "workflow_id": { + "type": "string", + "title": "Workflow Id", + "description": "The id of the workflow." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the workflow." + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + } + ], + "title": "Created At", + "description": "The created timestamp of the workflow." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + } + ], + "title": "Updated At", + "description": "The updated timestamp of the workflow." + }, + "opened_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Opened At", + "description": "The opened timestamp of the workflow." + }, + "user_id": { + "type": "string", + "title": "User Id", + "description": "The id of the user who owns this workflow." + }, + "is_public": { + "type": "boolean", + "title": "Is Public", + "description": "Whether this workflow is shared with all users." + }, + "workflow": { + "$ref": "#/components/schemas/Workflow", + "description": "The workflow." + }, + "thumbnail_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Thumbnail Url", + "description": "The URL of the workflow thumbnail." + } + }, + "type": "object", + "required": ["workflow_id", "name", "created_at", "updated_at", "user_id", "is_public", "workflow"], + "title": "WorkflowRecordWithThumbnailDTO" + }, + "WorkflowWithoutID": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the workflow." + }, + "author": { + "type": "string", + "title": "Author", + "description": "The author of the workflow." + }, + "description": { + "type": "string", + "title": "Description", + "description": "The description of the workflow." + }, + "version": { + "type": "string", + "title": "Version", + "description": "The version of the workflow." + }, + "contact": { + "type": "string", + "title": "Contact", + "description": "The contact of the workflow." + }, + "tags": { + "type": "string", + "title": "Tags", + "description": "The tags of the workflow." + }, + "notes": { + "type": "string", + "title": "Notes", + "description": "The notes of the workflow." + }, + "exposedFields": { + "items": { + "$ref": "#/components/schemas/ExposedField" + }, + "type": "array", + "title": "Exposedfields", + "description": "The exposed fields of the workflow." + }, + "meta": { + "$ref": "#/components/schemas/WorkflowMeta", + "description": "The meta of the workflow." + }, + "nodes": { + "items": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + "type": "array", + "title": "Nodes", + "description": "The nodes of the workflow." + }, + "edges": { + "items": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + "type": "array", + "title": "Edges", + "description": "The edges of the workflow." + }, + "form": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Form", + "description": "The form of the workflow." + } + }, + "type": "object", + "required": [ + "name", + "author", + "description", + "version", + "contact", + "tags", + "notes", + "exposedFields", + "meta", + "nodes", + "edges" + ], + "title": "WorkflowWithoutID" + }, + "ZImageConditioningField": { + "description": "A Z-Image conditioning tensor primitive value", + "properties": { + "conditioning_name": { + "description": "The name of conditioning tensor", + "title": "Conditioning Name", + "type": "string" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "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." + } + }, + "required": ["conditioning_name"], + "title": "ZImageConditioningField", + "type": "object" + }, + "ZImageConditioningOutput": { + "class": "output", + "description": "Base class for nodes that output a Z-Image text conditioning tensor.", + "properties": { + "conditioning": { + "$ref": "#/components/schemas/ZImageConditioningField", + "description": "Conditioning tensor", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "z_image_conditioning_output", + "default": "z_image_conditioning_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "conditioning", "type", "type"], + "title": "ZImageConditioningOutput", + "type": "object" + }, + "ZImageControlField": { + "description": "A Z-Image control conditioning field for spatial control (Canny, HED, Depth, Pose, MLSD).", + "properties": { + "image_name": { + "description": "The name of the preprocessed control image", + "title": "Image Name", + "type": "string" + }, + "control_model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "The Z-Image ControlNet adapter model" + }, + "control_context_scale": { + "default": 0.75, + "description": "The strength of the control signal. Recommended range: 0.65-0.80.", + "maximum": 2.0, + "minimum": 0.0, + "title": "Control Context Scale", + "type": "number" + }, + "begin_step_percent": { + "default": 0.0, + "description": "When the control is first applied (% of total steps)", + "maximum": 1.0, + "minimum": 0.0, + "title": "Begin Step Percent", + "type": "number" + }, + "end_step_percent": { + "default": 1.0, + "description": "When the control is last applied (% of total steps)", + "maximum": 1.0, + "minimum": 0.0, + "title": "End Step Percent", + "type": "number" + } + }, + "required": ["image_name", "control_model"], + "title": "ZImageControlField", + "type": "object" + }, + "ZImageControlInvocation": { + "category": "conditioning", + "class": "invocation", + "classification": "prototype", + "description": "Configure Z-Image ControlNet for spatial conditioning.\n\nTakes a preprocessed control image (e.g., Canny edges, depth map, pose)\nand a Z-Image ControlNet adapter model to enable spatial control.\n\nSupports 5 control modes: Canny, HED, Depth, Pose, MLSD.\nRecommended control_context_scale: 0.65-0.80.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The preprocessed control image (Canny, HED, Depth, Pose, or MLSD)", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "control_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "ControlNet model to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Control Model", + "ui_model_base": ["z-image"], + "ui_model_type": ["controlnet"] + }, + "control_context_scale": { + "default": 0.75, + "description": "Strength of the control signal. Recommended range: 0.65-0.80.", + "field_kind": "input", + "input": "any", + "maximum": 2.0, + "minimum": 0.0, + "orig_default": 0.75, + "orig_required": false, + "title": "Control Scale", + "type": "number" + }, + "begin_step_percent": { + "default": 0.0, + "description": "When the control is first applied (% of total steps)", + "field_kind": "input", + "input": "any", + "maximum": 1.0, + "minimum": 0.0, + "orig_default": 0.0, + "orig_required": false, + "title": "Begin Step Percent", + "type": "number" + }, + "end_step_percent": { + "default": 1.0, + "description": "When the control is last applied (% of total steps)", + "field_kind": "input", + "input": "any", + "maximum": 1.0, + "minimum": 0.0, + "orig_default": 1.0, + "orig_required": false, + "title": "End Step Percent", + "type": "number" + }, + "type": { + "const": "z_image_control", + "default": "z_image_control", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "z-image", "control", "controlnet"], + "title": "Z-Image ControlNet", + "type": "object", + "version": "1.1.0", + "output": { + "$ref": "#/components/schemas/ZImageControlOutput" + } + }, + "ZImageControlOutput": { + "class": "output", + "description": "Z-Image Control output containing control configuration.", + "properties": { + "control": { + "$ref": "#/components/schemas/ZImageControlField", + "description": "Z-Image control conditioning", + "field_kind": "output", + "ui_hidden": false + }, + "type": { + "const": "z_image_control_output", + "default": "z_image_control_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "control", "type", "type"], + "title": "ZImageControlOutput", + "type": "object" + }, + "ZImageDenoiseInvocation": { + "category": "latents", + "class": "invocation", + "classification": "prototype", + "description": "Run the denoising process with a Z-Image model.\n\nSupports regional prompting by connecting multiple conditioning inputs with masks.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "noise": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Noise tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoise_mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/DenoiseMaskField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "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.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoising_start": { + "default": 0.0, + "description": "When to start denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0.0, + "orig_required": false, + "title": "Denoising Start", + "type": "number" + }, + "denoising_end": { + "default": 1.0, + "description": "When to stop denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Denoising End", + "type": "number" + }, + "add_noise": { + "default": true, + "description": "Add noise based on denoising start.", + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "Add Noise", + "type": "boolean" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Z-Image model (Transformer) to load", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Transformer" + }, + "positive_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/ZImageConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/ZImageConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Positive conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Positive Conditioning" + }, + "negative_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/ZImageConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/ZImageConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Negative conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Negative Conditioning" + }, + "guidance_scale": { + "default": 1.0, + "description": "Guidance scale for classifier-free guidance. 1.0 = no CFG (recommended for Z-Image-Turbo). Values > 1.0 amplify guidance.", + "field_kind": "input", + "input": "any", + "minimum": 1.0, + "orig_default": 1.0, + "orig_required": false, + "title": "Guidance Scale", + "type": "number" + }, + "width": { + "default": 1024, + "description": "Width of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 16, + "orig_default": 1024, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 1024, + "description": "Height of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 16, + "orig_default": 1024, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "steps": { + "default": 8, + "description": "Number of denoising steps. 8 recommended for Z-Image-Turbo.", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 8, + "orig_required": false, + "title": "Steps", + "type": "integer" + }, + "seed": { + "default": 0, + "description": "Randomness seed for reproducibility.", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Seed", + "type": "integer" + }, + "control": { + "anyOf": [ + { + "$ref": "#/components/schemas/ZImageControlField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Z-Image control conditioning for spatial control (Canny, HED, Depth, Pose, MLSD).", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE Required for control conditioning.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "shift": { + "anyOf": [ + { + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "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.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Shift" + }, + "scheduler": { + "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).", + "enum": ["euler", "heun", "lcm"], + "field_kind": "input", + "input": "any", + "orig_default": "euler", + "orig_required": false, + "title": "Scheduler", + "type": "string", + "ui_choice_labels": { + "euler": "Euler", + "heun": "Heun (2nd order)", + "lcm": "LCM" + } + }, + "type": { + "const": "z_image_denoise", + "default": "z_image_denoise", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "z-image"], + "title": "Denoise - Z-Image", + "type": "object", + "version": "1.6.0", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "ZImageDenoiseMetaInvocation": { + "category": "metadata", + "class": "invocation", + "classification": "stable", + "description": "Run denoising process with a Z-Image transformer model + metadata.", + "node_pack": "invokeai", + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "noise": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Noise tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoise_mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/DenoiseMaskField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "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.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "denoising_start": { + "default": 0.0, + "description": "When to start denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 0.0, + "orig_required": false, + "title": "Denoising Start", + "type": "number" + }, + "denoising_end": { + "default": 1.0, + "description": "When to stop denoising, expressed a percentage of total steps", + "field_kind": "input", + "input": "any", + "maximum": 1, + "minimum": 0, + "orig_default": 1.0, + "orig_required": false, + "title": "Denoising End", + "type": "number" + }, + "add_noise": { + "default": true, + "description": "Add noise based on denoising start.", + "field_kind": "input", + "input": "any", + "orig_default": true, + "orig_required": false, + "title": "Add Noise", + "type": "boolean" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Z-Image model (Transformer) to load", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Transformer" + }, + "positive_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/ZImageConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/ZImageConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Positive conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Positive Conditioning" + }, + "negative_conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/ZImageConditioningField" + }, + { + "items": { + "$ref": "#/components/schemas/ZImageConditioningField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Negative conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Negative Conditioning" + }, + "guidance_scale": { + "default": 1.0, + "description": "Guidance scale for classifier-free guidance. 1.0 = no CFG (recommended for Z-Image-Turbo). Values > 1.0 amplify guidance.", + "field_kind": "input", + "input": "any", + "minimum": 1.0, + "orig_default": 1.0, + "orig_required": false, + "title": "Guidance Scale", + "type": "number" + }, + "width": { + "default": 1024, + "description": "Width of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 16, + "orig_default": 1024, + "orig_required": false, + "title": "Width", + "type": "integer" + }, + "height": { + "default": 1024, + "description": "Height of the generated image.", + "field_kind": "input", + "input": "any", + "multipleOf": 16, + "orig_default": 1024, + "orig_required": false, + "title": "Height", + "type": "integer" + }, + "steps": { + "default": 8, + "description": "Number of denoising steps. 8 recommended for Z-Image-Turbo.", + "exclusiveMinimum": 0, + "field_kind": "input", + "input": "any", + "orig_default": 8, + "orig_required": false, + "title": "Steps", + "type": "integer" + }, + "seed": { + "default": 0, + "description": "Randomness seed for reproducibility.", + "field_kind": "input", + "input": "any", + "orig_default": 0, + "orig_required": false, + "title": "Seed", + "type": "integer" + }, + "control": { + "anyOf": [ + { + "$ref": "#/components/schemas/ZImageControlField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Z-Image control conditioning for spatial control (Canny, HED, Depth, Pose, MLSD).", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE Required for control conditioning.", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false + }, + "shift": { + "anyOf": [ + { + "minimum": 0.0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "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.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "Shift" + }, + "scheduler": { + "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).", + "enum": ["euler", "heun", "lcm"], + "field_kind": "input", + "input": "any", + "orig_default": "euler", + "orig_required": false, + "title": "Scheduler", + "type": "string", + "ui_choice_labels": { + "euler": "Euler", + "heun": "Heun (2nd order)", + "lcm": "LCM" + } + }, + "type": { + "const": "z_image_denoise_meta", + "default": "z_image_denoise_meta", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["z-image", "latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"], + "title": "Denoise - Z-Image + Metadata", + "type": "object", + "version": "1.1.0", + "output": { + "$ref": "#/components/schemas/LatentsMetaOutput" + } + }, + "ZImageImageToLatentsInvocation": { + "category": "latents", + "class": "invocation", + "classification": "prototype", + "description": "Generates latents from an image using Z-Image VAE (supports both Diffusers and FLUX VAE).", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The image to encode.", + "field_kind": "input", + "input": "any", + "orig_required": true + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "type": { + "const": "z_image_i2l", + "default": "z_image_i2l", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["image", "latents", "vae", "i2l", "z-image"], + "title": "Image to Latents - Z-Image", + "type": "object", + "version": "1.1.0", + "output": { + "$ref": "#/components/schemas/LatentsOutput" + } + }, + "ZImageLatentsToImageInvocation": { + "category": "latents", + "class": "invocation", + "classification": "prototype", + "description": "Generates an image from latents using Z-Image VAE (supports both Diffusers and FLUX VAE).", + "node_pack": "invokeai", + "properties": { + "board": { + "anyOf": [ + { + "$ref": "#/components/schemas/BoardField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The board to save the image to", + "field_kind": "internal", + "input": "direct", + "orig_required": false, + "ui_hidden": false + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MetadataField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata to be saved with the image", + "field_kind": "internal", + "input": "connection", + "orig_required": false, + "ui_hidden": false + }, + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "latents": { + "anyOf": [ + { + "$ref": "#/components/schemas/LatentsField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Latents tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "vae": { + "anyOf": [ + { + "$ref": "#/components/schemas/VAEField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "VAE", + "field_kind": "input", + "input": "connection", + "orig_required": true + }, + "type": { + "const": "z_image_l2i", + "default": "z_image_l2i", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["latents", "image", "vae", "l2i", "z-image"], + "title": "Latents to Image - Z-Image", + "type": "object", + "version": "1.1.0", + "output": { + "$ref": "#/components/schemas/ImageOutput" + } + }, + "ZImageLoRACollectionLoader": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Applies a collection of LoRAs to a Z-Image transformer.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "loras": { + "anyOf": [ + { + "$ref": "#/components/schemas/LoRAField" + }, + { + "items": { + "$ref": "#/components/schemas/LoRAField" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LoRA models and weights. May be a single LoRA or collection.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false, + "title": "LoRAs" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Transformer", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Transformer" + }, + "qwen3_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/Qwen3EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Qwen3 tokenizer and text encoder", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Qwen3 Encoder" + }, + "type": { + "const": "z_image_lora_collection_loader", + "default": "z_image_lora_collection_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["lora", "model", "z-image"], + "title": "Apply LoRA Collection - Z-Image", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ZImageLoRALoaderOutput" + } + }, + "ZImageLoRALoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "stable", + "description": "Apply a LoRA model to a Z-Image transformer and/or Qwen3 text encoder.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "lora": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LoRA model to load", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "LoRA", + "ui_model_base": ["z-image"], + "ui_model_type": ["lora"] + }, + "weight": { + "default": 0.75, + "description": "The weight at which the LoRA is applied to each model", + "field_kind": "input", + "input": "any", + "orig_default": 0.75, + "orig_required": false, + "title": "Weight", + "type": "number" + }, + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Transformer", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Z-Image Transformer" + }, + "qwen3_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/Qwen3EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Qwen3 tokenizer and text encoder", + "field_kind": "input", + "input": "connection", + "orig_default": null, + "orig_required": false, + "title": "Qwen3 Encoder" + }, + "type": { + "const": "z_image_lora_loader", + "default": "z_image_lora_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["lora", "model", "z-image"], + "title": "Apply LoRA - Z-Image", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ZImageLoRALoaderOutput" + } + }, + "ZImageLoRALoaderOutput": { + "class": "output", + "description": "Z-Image LoRA Loader Output", + "properties": { + "transformer": { + "anyOf": [ + { + "$ref": "#/components/schemas/TransformerField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Transformer", + "field_kind": "output", + "title": "Z-Image Transformer", + "ui_hidden": false + }, + "qwen3_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/Qwen3EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Qwen3 tokenizer and text encoder", + "field_kind": "output", + "title": "Qwen3 Encoder", + "ui_hidden": false + }, + "type": { + "const": "z_image_lora_loader_output", + "default": "z_image_lora_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "transformer", "qwen3_encoder", "type", "type"], + "title": "ZImageLoRALoaderOutput", + "type": "object" + }, + "ZImageModelLoaderInvocation": { + "category": "model", + "class": "invocation", + "classification": "prototype", + "description": "Loads a Z-Image model, outputting its submodels.\n\nSimilar to FLUX, you can mix and match components:\n- Transformer: From Z-Image main model (GGUF quantized or Diffusers format)\n- VAE: Separate FLUX VAE (shared with FLUX models) or from a Diffusers Z-Image model\n- Qwen3 Encoder: Separate Qwen3Encoder model or from a Diffusers Z-Image model", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "model": { + "$ref": "#/components/schemas/ModelIdentifierField", + "description": "Z-Image model (Transformer) to load", + "field_kind": "input", + "input": "direct", + "orig_required": true, + "title": "Transformer", + "ui_model_base": ["z-image"], + "ui_model_type": ["main"] + }, + "vae_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "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.", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "VAE", + "ui_model_base": ["flux"], + "ui_model_type": ["vae"] + }, + "qwen3_encoder_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Standalone Qwen3 Encoder model. If not provided, encoder will be loaded from the Qwen3 Source model.", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Qwen3 Encoder", + "ui_model_type": ["qwen3_encoder"] + }, + "qwen3_source_model": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModelIdentifierField" + }, + { + "type": "null" + } + ], + "default": null, + "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.", + "field_kind": "input", + "input": "direct", + "orig_default": null, + "orig_required": false, + "title": "Qwen3 Source (Diffusers)", + "ui_model_base": ["z-image"], + "ui_model_format": ["diffusers"], + "ui_model_type": ["main"] + }, + "type": { + "const": "z_image_model_loader", + "default": "z_image_model_loader", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["model", "type", "id"], + "tags": ["model", "z-image"], + "title": "Main Model - Z-Image", + "type": "object", + "version": "3.0.0", + "output": { + "$ref": "#/components/schemas/ZImageModelLoaderOutput" + } + }, + "ZImageModelLoaderOutput": { + "class": "output", + "description": "Z-Image base model loader output.", + "properties": { + "transformer": { + "$ref": "#/components/schemas/TransformerField", + "description": "Transformer", + "field_kind": "output", + "title": "Transformer", + "ui_hidden": false + }, + "qwen3_encoder": { + "$ref": "#/components/schemas/Qwen3EncoderField", + "description": "Qwen3 tokenizer and text encoder", + "field_kind": "output", + "title": "Qwen3 Encoder", + "ui_hidden": false + }, + "vae": { + "$ref": "#/components/schemas/VAEField", + "description": "VAE", + "field_kind": "output", + "title": "VAE", + "ui_hidden": false + }, + "type": { + "const": "z_image_model_loader_output", + "default": "z_image_model_loader_output", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["output_meta", "transformer", "qwen3_encoder", "vae", "type", "type"], + "title": "ZImageModelLoaderOutput", + "type": "object" + }, + "ZImageSeedVarianceEnhancerInvocation": { + "category": "prompt", + "class": "invocation", + "classification": "prototype", + "description": "Adds seed-based noise to Z-Image conditioning to increase variance between seeds.\n\nZ-Image-Turbo can produce relatively similar images with different seeds,\nmaking it harder to explore variations of a prompt. This node implements\nreproducible, seed-based noise injection into text embeddings to increase\nvisual variation while maintaining reproducibility.\n\nThe noise strength is auto-calibrated relative to the embedding's standard\ndeviation, ensuring consistent results across different prompts.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "conditioning": { + "anyOf": [ + { + "$ref": "#/components/schemas/ZImageConditioningField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Conditioning tensor", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Conditioning" + }, + "seed": { + "default": 0, + "description": "Seed for reproducible noise generation. Different seeds produce different noise patterns.", + "field_kind": "input", + "input": "any", + "minimum": 0, + "orig_default": 0, + "orig_required": false, + "title": "Seed", + "type": "integer" + }, + "strength": { + "default": 0.1, + "description": "Noise strength as multiplier of embedding std. 0=off, 0.1=subtle, 0.5=strong.", + "field_kind": "input", + "input": "any", + "maximum": 2.0, + "minimum": 0.0, + "orig_default": 0.1, + "orig_required": false, + "title": "Strength", + "type": "number" + }, + "randomize_percent": { + "default": 50.0, + "description": "Percentage of embedding values to add noise to (1-100). Lower values create more selective noise patterns.", + "field_kind": "input", + "input": "any", + "maximum": 100.0, + "minimum": 1.0, + "orig_default": 50.0, + "orig_required": false, + "title": "Randomize Percent", + "type": "number" + }, + "type": { + "const": "z_image_seed_variance_enhancer", + "default": "z_image_seed_variance_enhancer", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["conditioning", "z-image", "variance", "seed"], + "title": "Seed Variance Enhancer - Z-Image", + "type": "object", + "version": "1.0.0", + "output": { + "$ref": "#/components/schemas/ZImageConditioningOutput" + } + }, + "ZImageTextEncoderInvocation": { + "category": "prompt", + "class": "invocation", + "classification": "prototype", + "description": "Encodes and preps a prompt for a Z-Image image.\n\nSupports regional prompting by connecting a mask input.", + "node_pack": "invokeai", + "properties": { + "id": { + "description": "The id of this instance of an invocation. Must be unique among all instances of invocations.", + "field_kind": "node_attribute", + "title": "Id", + "type": "string" + }, + "is_intermediate": { + "default": false, + "description": "Whether or not this is an intermediate invocation.", + "field_kind": "node_attribute", + "input": "direct", + "orig_required": true, + "title": "Is Intermediate", + "type": "boolean", + "ui_hidden": false, + "ui_type": "IsIntermediate" + }, + "use_cache": { + "default": true, + "description": "Whether or not to use the cache", + "field_kind": "node_attribute", + "title": "Use Cache", + "type": "boolean" + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Text prompt to encode.", + "field_kind": "input", + "input": "any", + "orig_required": true, + "title": "Prompt", + "ui_component": "textarea" + }, + "qwen3_encoder": { + "anyOf": [ + { + "$ref": "#/components/schemas/Qwen3EncoderField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Qwen3 tokenizer and text encoder", + "field_kind": "input", + "input": "connection", + "orig_required": true, + "title": "Qwen3 Encoder" + }, + "mask": { + "anyOf": [ + { + "$ref": "#/components/schemas/TensorField" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A mask defining the region that this conditioning prompt applies to.", + "field_kind": "input", + "input": "any", + "orig_default": null, + "orig_required": false + }, + "type": { + "const": "z_image_text_encoder", + "default": "z_image_text_encoder", + "field_kind": "node_attribute", + "title": "type", + "type": "string" + } + }, + "required": ["type", "id"], + "tags": ["prompt", "conditioning", "z-image"], + "title": "Prompt - Z-Image", + "type": "object", + "version": "1.1.0", + "output": { + "$ref": "#/components/schemas/ZImageConditioningOutput" + } + }, + "ZImageVariantType": { + "type": "string", + "enum": ["turbo", "zbase"], + "title": "ZImageVariantType", + "description": "Z-Image model variants." + } + }, + "securitySchemes": { + "HTTPBearer": { + "type": "http", + "scheme": "bearer" + } + } + } +} diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json new file mode 100644 index 00000000000..e537362801c --- /dev/null +++ b/invokeai/frontend/web/package.json @@ -0,0 +1,162 @@ +{ + "name": "@invoke-ai/invoke-ai-ui", + "private": true, + "version": "0.0.1", + "publishConfig": { + "access": "restricted", + "registry": "https://npm.pkg.github.com" + }, + "main": "./dist/invoke-ai-ui.umd.js", + "module": "./dist/invoke-ai-ui.es.js", + "exports": { + ".": { + "import": "./dist/invoke-ai-ui.es.js", + "require": "./dist/invoke-ai-ui.umd.js" + } + }, + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "dev": "vite dev", + "dev:host": "vite dev --host", + "build": "pnpm run lint && vitest run && vite build", + "typegen": "node scripts/typegen.js", + "preview": "vite preview", + "lint:knip": "knip --tags=-knipignore", + "lint:dpdm": "dpdm --no-warning --no-tree --transform --exit-code circular:1 src/main.tsx", + "lint:eslint": "eslint --max-warnings=0 .", + "lint:prettier": "prettier --check .", + "lint:tsc": "tsc --noEmit", + "lint": "concurrently -g -c red,green,yellow,blue,magenta pnpm:lint:*", + "fix": "eslint --fix . && prettier --log-level warn --write .", + "preinstall": "npx only-allow pnpm", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", + "test": "vitest", + "test:run": "vitest run", + "test:ui": "vitest --coverage --ui", + "test:no-watch": "vitest --no-watch" + }, + "dependencies": { + "@atlaskit/pragmatic-drag-and-drop": "^1.7.4", + "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.1", + "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0", + "@dagrejs/dagre": "^1.1.5", + "@dagrejs/graphlib": "^2.2.4", + "@fontsource-variable/inter": "^5.2.6", + "@invoke-ai/ui-library": "github:invoke-ai/ui-library#v0.0.48", + "@nanostores/react": "^1.0.0", + "@observ33r/object-equals": "^1.1.5", + "@reduxjs/toolkit": "2.8.2", + "@roarr/browser-log-writer": "^1.3.0", + "@xyflow/react": "^12.8.2", + "ag-psd": "^28.2.2", + "async-mutex": "^0.5.0", + "chakra-react-select": "^4.9.2", + "cmdk": "^1.1.1", + "compare-versions": "^6.1.1", + "dockview": "^4.7.1", + "es-toolkit": "^1.39.7", + "filesize": "^10.1.6", + "fracturedjsonjs": "^4.1.0", + "framer-motion": "^11.10.0", + "i18next": "^25.3.2", + "i18next-http-backend": "^3.0.2", + "idb-keyval": "6.2.1", + "jsondiffpatch": "^0.7.3", + "jszip": "^3.10.1", + "konva": "^9.3.22", + "linkify-react": "^4.3.1", + "linkifyjs": "^4.3.1", + "lru-cache": "^11.1.0", + "mtwist": "^1.0.2", + "nanoid": "^5.1.5", + "nanostores": "^1.0.1", + "new-github-issue-url": "^1.1.0", + "overlayscrollbars": "^2.11.4", + "overlayscrollbars-react": "^0.5.6", + "perfect-freehand": "^1.2.2", + "query-string": "^9.2.1", + "raf-throttle": "^2.0.6", + "react": "^18.3.1", + "react-colorful": "^5.6.1", + "react-dom": "^18.3.1", + "react-dropzone": "^14.3.8", + "react-error-boundary": "^5.0.0", + "react-hook-form": "^7.60.0", + "react-hotkeys-hook": "4.5.0", + "react-i18next": "^15.5.3", + "react-icons": "^5.5.0", + "react-redux": "9.2.0", + "react-resizable-panels": "^3.0.3", + "react-router-dom": "^7.12.0", + "react-textarea-autosize": "^8.5.9", + "react-use": "^17.6.0", + "react-virtuoso": "^4.13.0", + "redux-remember": "^5.2.0", + "redux-undo": "^1.1.0", + "rfdc": "^1.4.1", + "roarr": "^7.21.1", + "serialize-error": "^12.0.0", + "socket.io-client": "^4.8.1", + "stable-hash": "^0.0.6", + "use-debounce": "^10.0.5", + "use-device-pixel-ratio": "^1.1.2", + "uuid": "^11.1.0", + "zod": "^4.0.10", + "zod-validation-error": "^3.5.2" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.31.0", + "@storybook/addon-docs": "^9.0.17", + "@storybook/addon-links": "^9.0.17", + "@storybook/react-vite": "^9.0.17", + "@types/node": "^22.15.1", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.0", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^8.37.0", + "@typescript-eslint/parser": "^8.37.0", + "@vitejs/plugin-react-swc": "^3.9.0", + "@vitest/coverage-v8": "^3.1.2", + "@vitest/ui": "^3.1.2", + "concurrently": "^9.1.2", + "csstype": "^3.1.3", + "dpdm": "^3.14.0", + "eslint": "^9.31.0", + "eslint-plugin-i18next": "^6.1.2", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-path": "^2.0.3", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.5", + "eslint-plugin-simple-import-sort": "^12.0.0", + "eslint-plugin-storybook": "^9.0.17", + "eslint-plugin-unused-imports": "^4.1.4", + "globals": "^16.3.0", + "knip": "^5.61.3", + "magic-string": "^0.30.17", + "openapi-types": "^12.1.3", + "openapi-typescript": "^7.6.1", + "prettier": "^3.5.3", + "rollup-plugin-visualizer": "^6.0.3", + "storybook": "^9.0.17", + "tsafe": "^1.8.5", + "type-fest": "^4.40.0", + "typescript": "^5.8.3", + "vite": "^7.0.5", + "vite-plugin-eslint": "^1.8.1", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.1.2" + }, + "engines": { + "pnpm": "10" + }, + "packageManager": "pnpm@10.12.4" +} diff --git a/invokeai/frontend/web/patches/reselect@5.0.1.patch b/invokeai/frontend/web/patches/reselect@5.0.1.patch new file mode 100644 index 00000000000..75d25308b96 --- /dev/null +++ b/invokeai/frontend/web/patches/reselect@5.0.1.patch @@ -0,0 +1,241 @@ +diff --git a/dist/cjs/reselect.cjs b/dist/cjs/reselect.cjs +index 0ef3a648e253af4ada8f0a2086d6db9302b8ced9..2614db8c901c5a3be4a80d3ffed3be2cf175bf50 100644 +--- a/dist/cjs/reselect.cjs ++++ b/dist/cjs/reselect.cjs +@@ -639,6 +639,8 @@ function weakMapMemoize(func, options = {}) { + return memoized; + } + ++weakMapMemoize = lruMemoize ++ + // src/createSelectorCreator.ts + function createSelectorCreator(memoizeOrOptions, ...memoizeOptionsFromArgs) { + const createSelectorCreatorOptions = typeof memoizeOrOptions === "function" ? { +diff --git a/dist/reselect.browser.mjs b/dist/reselect.browser.mjs +index e8da6c11a333ef9ddf4cca51adbc405fe8f6265d..8bc64f0c19082c0015155d60c59869a46c9f180e 100644 +--- a/dist/reselect.browser.mjs ++++ b/dist/reselect.browser.mjs +@@ -1,2 +1,2 @@ +-var oe={inputStabilityCheck:"once",identityFunctionCheck:"once"},re=e=>{Object.assign(oe,e)};var M="NOT_FOUND";function w(e,t=`expected a function, instead received ${typeof e}`){if(typeof e!="function")throw new TypeError(t)}function V(e,t=`expected an object, instead received ${typeof e}`){if(typeof e!="object")throw new TypeError(t)}function ie(e,t="expected all items to be functions, instead received the following types: "){if(!e.every(n=>typeof n=="function")){let n=e.map(c=>typeof c=="function"?`function ${c.name||"unnamed"}()`:typeof c).join(", ");throw new TypeError(`${t}[${n}]`)}}var O=e=>Array.isArray(e)?e:[e];function K(e){let t=Array.isArray(e[0])?e[0]:e;return ie(t,"createSelector expects all input-selectors to be functions, but received the following types: "),t}function W(e,t){let n=[],{length:c}=e;for(let s=0;sthis._cachedRevision){let{fn:t}=this,n=new Set,c=S;S=n,this._cachedValue=t(),S=c,this.hits++,this._deps=Array.from(n),this._cachedRevision=this.revision}return S?.add(this),this._cachedValue}get revision(){return Math.max(...this._deps.map(t=>t.revision),0)}};function g(e){return e instanceof F||console.warn("Not a valid cell! ",e),e.value}function L(e,t){if(!(e instanceof F))throw new TypeError("setValue must be passed a tracked store created with `createStorage`.");e.value=e._lastValue=t}function $(e,t=v){return new F(e,t)}function Y(e){return w(e,"the first parameter to `createCache` must be a function"),new b(e)}var ce=(e,t)=>!1;function z(){return $(null,ce)}function k(e,t){L(e,t)}var A=e=>{let t=e.collectionTag;t===null&&(t=e.collectionTag=z()),g(t)},h=e=>{let t=e.collectionTag;t!==null&&k(t,null)};var Re=Symbol(),H=0,se=Object.getPrototypeOf({}),I=class{constructor(t){this.value=t;this.value=t,this.tag.value=t}proxy=new Proxy(this,C);tag=z();tags={};children={};collectionTag=null;id=H++},C={get(e,t){function n(){let{value:s}=e,o=Reflect.get(s,t);if(typeof t=="symbol"||t in se)return o;if(typeof o=="object"&&o!==null){let i=e.children[t];return i===void 0&&(i=e.children[t]=E(o)),i.tag&&g(i.tag),i.proxy}else{let i=e.tags[t];return i===void 0&&(i=e.tags[t]=z(),i.value=o),g(i),o}}return n()},ownKeys(e){return A(e),Reflect.ownKeys(e.value)},getOwnPropertyDescriptor(e,t){return Reflect.getOwnPropertyDescriptor(e.value,t)},has(e,t){return Reflect.has(e.value,t)}},N=class{constructor(t){this.value=t;this.value=t,this.tag.value=t}proxy=new Proxy([this],ue);tag=z();tags={};children={};collectionTag=null;id=H++},ue={get([e],t){return t==="length"&&A(e),C.get(e,t)},ownKeys([e]){return C.ownKeys(e)},getOwnPropertyDescriptor([e],t){return C.getOwnPropertyDescriptor(e,t)},has([e],t){return C.has(e,t)}};function E(e){return Array.isArray(e)?new N(e):new I(e)}function D(e,t){let{value:n,tags:c,children:s}=e;if(e.value=t,Array.isArray(n)&&Array.isArray(t)&&n.length!==t.length)h(e);else if(n!==t){let o=0,i=0,r=!1;for(let u in n)o++;for(let u in t)if(i++,!(u in n)){r=!0;break}(r||o!==i)&&h(e)}for(let o in c){let i=n[o],r=t[o];i!==r&&(h(e),k(c[o],r)),typeof r=="object"&&r!==null&&delete c[o]}for(let o in s){let i=s[o],r=t[o];i.value!==r&&(typeof r=="object"&&r!==null?D(i,r):(X(i),delete s[o]))}}function X(e){e.tag&&k(e.tag,null),h(e);for(let t in e.tags)k(e.tags[t],null);for(let t in e.children)X(e.children[t])}function le(e){let t;return{get(n){return t&&e(t.key,n)?t.value:M},put(n,c){t={key:n,value:c}},getEntries(){return t?[t]:[]},clear(){t=void 0}}}function ae(e,t){let n=[];function c(r){let l=n.findIndex(u=>t(r,u.key));if(l>-1){let u=n[l];return l>0&&(n.splice(l,1),n.unshift(u)),u.value}return M}function s(r,l){c(r)===M&&(n.unshift({key:r,value:l}),n.length>e&&n.pop())}function o(){return n}function i(){n=[]}return{get:c,put:s,getEntries:o,clear:i}}var x=(e,t)=>e===t;function j(e){return function(n,c){if(n===null||c===null||n.length!==c.length)return!1;let{length:s}=n;for(let o=0;oo(p.value,a));f&&(a=f.value,r!==0&&r--)}l.put(arguments,a)}return a}return u.clearCache=()=>{l.clear(),u.resetResultsCount()},u.resultsCount=()=>r,u.resetResultsCount=()=>{r=0},u}function me(e){let t=E([]),n=null,c=j(x),s=Y(()=>e.apply(null,t.proxy));function o(){return c(n,arguments)||(D(t,arguments),n=arguments),s.value}return o.clearCache=()=>s.clear(),o}var _=class{constructor(t){this.value=t}deref(){return this.value}},de=typeof WeakRef<"u"?WeakRef:_,fe=0,B=1;function T(){return{s:fe,v:void 0,o:null,p:null}}function R(e,t={}){let n=T(),{resultEqualityCheck:c}=t,s,o=0;function i(){let r=n,{length:l}=arguments;for(let m=0,f=l;m{n=T(),i.resetResultsCount()},i.resultsCount=()=>o,i.resetResultsCount=()=>{o=0},i}function J(e,...t){let n=typeof e=="function"?{memoize:e,memoizeOptions:t}:e;return(...s)=>{let o=0,i=0,r,l={},u=s.pop();typeof u=="object"&&(l=u,u=s.pop()),w(u,`createSelector expects an output function after the inputs, but received: [${typeof u}]`);let a={...n,...l},{memoize:m,memoizeOptions:f=[],argsMemoize:p=R,argsMemoizeOptions:d=[],devModeChecks:y={}}=a,Q=O(f),Z=O(d),q=K(s),P=m(function(){return o++,u.apply(null,arguments)},...Q),Me=!0,ee=p(function(){i++;let ne=W(q,arguments);return r=P.apply(null,ne),r},...Z);return Object.assign(ee,{resultFunc:u,memoizedResultFunc:P,dependencies:q,dependencyRecomputations:()=>i,resetDependencyRecomputations:()=>{i=0},lastResult:()=>r,recomputations:()=>o,resetRecomputations:()=>{o=0},memoize:m,argsMemoize:p})}}var U=J(R);var ye=(e,t=U)=>{V(e,`createStructuredSelector expects first argument to be an object where each property is a selector, instead received a ${typeof e}`);let n=Object.keys(e),c=n.map(o=>e[o]);return t(c,(...o)=>o.reduce((i,r,l)=>(i[n[l]]=r,i),{}))};export{U as createSelector,J as createSelectorCreator,ye as createStructuredSelector,pe as lruMemoize,x as referenceEqualityCheck,re as setGlobalDevModeChecks,me as unstable_autotrackMemoize,R as weakMapMemoize}; ++var oe={inputStabilityCheck:"once",identityFunctionCheck:"once"},re=e=>{Object.assign(oe,e)};var M="NOT_FOUND";function w(e,t=`expected a function, instead received ${typeof e}`){if(typeof e!="function")throw new TypeError(t)}function V(e,t=`expected an object, instead received ${typeof e}`){if(typeof e!="object")throw new TypeError(t)}function ie(e,t="expected all items to be functions, instead received the following types: "){if(!e.every(n=>typeof n=="function")){let n=e.map(c=>typeof c=="function"?`function ${c.name||"unnamed"}()`:typeof c).join(", ");throw new TypeError(`${t}[${n}]`)}}var O=e=>Array.isArray(e)?e:[e];function K(e){let t=Array.isArray(e[0])?e[0]:e;return ie(t,"createSelector expects all input-selectors to be functions, but received the following types: "),t}function W(e,t){let n=[],{length:c}=e;for(let s=0;sthis._cachedRevision){let{fn:t}=this,n=new Set,c=S;S=n,this._cachedValue=t(),S=c,this.hits++,this._deps=Array.from(n),this._cachedRevision=this.revision}return S?.add(this),this._cachedValue}get revision(){return Math.max(...this._deps.map(t=>t.revision),0)}};function g(e){return e instanceof F||console.warn("Not a valid cell! ",e),e.value}function L(e,t){if(!(e instanceof F))throw new TypeError("setValue must be passed a tracked store created with `createStorage`.");e.value=e._lastValue=t}function $(e,t=v){return new F(e,t)}function Y(e){return w(e,"the first parameter to `createCache` must be a function"),new b(e)}var ce=(e,t)=>!1;function z(){return $(null,ce)}function k(e,t){L(e,t)}var A=e=>{let t=e.collectionTag;t===null&&(t=e.collectionTag=z()),g(t)},h=e=>{let t=e.collectionTag;t!==null&&k(t,null)};var Re=Symbol(),H=0,se=Object.getPrototypeOf({}),I=class{constructor(t){this.value=t;this.value=t,this.tag.value=t}proxy=new Proxy(this,C);tag=z();tags={};children={};collectionTag=null;id=H++},C={get(e,t){function n(){let{value:s}=e,o=Reflect.get(s,t);if(typeof t=="symbol"||t in se)return o;if(typeof o=="object"&&o!==null){let i=e.children[t];return i===void 0&&(i=e.children[t]=E(o)),i.tag&&g(i.tag),i.proxy}else{let i=e.tags[t];return i===void 0&&(i=e.tags[t]=z(),i.value=o),g(i),o}}return n()},ownKeys(e){return A(e),Reflect.ownKeys(e.value)},getOwnPropertyDescriptor(e,t){return Reflect.getOwnPropertyDescriptor(e.value,t)},has(e,t){return Reflect.has(e.value,t)}},N=class{constructor(t){this.value=t;this.value=t,this.tag.value=t}proxy=new Proxy([this],ue);tag=z();tags={};children={};collectionTag=null;id=H++},ue={get([e],t){return t==="length"&&A(e),C.get(e,t)},ownKeys([e]){return C.ownKeys(e)},getOwnPropertyDescriptor([e],t){return C.getOwnPropertyDescriptor(e,t)},has([e],t){return C.has(e,t)}};function E(e){return Array.isArray(e)?new N(e):new I(e)}function D(e,t){let{value:n,tags:c,children:s}=e;if(e.value=t,Array.isArray(n)&&Array.isArray(t)&&n.length!==t.length)h(e);else if(n!==t){let o=0,i=0,r=!1;for(let u in n)o++;for(let u in t)if(i++,!(u in n)){r=!0;break}(r||o!==i)&&h(e)}for(let o in c){let i=n[o],r=t[o];i!==r&&(h(e),k(c[o],r)),typeof r=="object"&&r!==null&&delete c[o]}for(let o in s){let i=s[o],r=t[o];i.value!==r&&(typeof r=="object"&&r!==null?D(i,r):(X(i),delete s[o]))}}function X(e){e.tag&&k(e.tag,null),h(e);for(let t in e.tags)k(e.tags[t],null);for(let t in e.children)X(e.children[t])}function le(e){let t;return{get(n){return t&&e(t.key,n)?t.value:M},put(n,c){t={key:n,value:c}},getEntries(){return t?[t]:[]},clear(){t=void 0}}}function ae(e,t){let n=[];function c(r){let l=n.findIndex(u=>t(r,u.key));if(l>-1){let u=n[l];return l>0&&(n.splice(l,1),n.unshift(u)),u.value}return M}function s(r,l){c(r)===M&&(n.unshift({key:r,value:l}),n.length>e&&n.pop())}function o(){return n}function i(){n=[]}return{get:c,put:s,getEntries:o,clear:i}}var x=(e,t)=>e===t;function j(e){return function(n,c){if(n===null||c===null||n.length!==c.length)return!1;let{length:s}=n;for(let o=0;oo(p.value,a));f&&(a=f.value,r!==0&&r--)}l.put(arguments,a)}return a}return u.clearCache=()=>{l.clear(),u.resetResultsCount()},u.resultsCount=()=>r,u.resetResultsCount=()=>{r=0},u}function me(e){let t=E([]),n=null,c=j(x),s=Y(()=>e.apply(null,t.proxy));function o(){return c(n,arguments)||(D(t,arguments),n=arguments),s.value}return o.clearCache=()=>s.clear(),o}var _=class{constructor(t){this.value=t}deref(){return this.value}},de=typeof WeakRef<"u"?WeakRef:_,fe=0,B=1;function T(){return{s:fe,v:void 0,o:null,p:null}}function R(e,t={}){let n=T(),{resultEqualityCheck:c}=t,s,o=0;function i(){let r=n,{length:l}=arguments;for(let m=0,f=l;m{n=T(),i.resetResultsCount()},i.resultsCount=()=>o,i.resetResultsCount=()=>{o=0},i}function J(e,...t){let n=typeof e=="function"?{memoize:e,memoizeOptions:t}:e;return(...s)=>{let o=0,i=0,r,l={},u=s.pop();typeof u=="object"&&(l=u,u=s.pop()),w(u,`createSelector expects an output function after the inputs, but received: [${typeof u}]`);let a={...n,...l},{memoize:m,memoizeOptions:f=[],argsMemoize:p=R,argsMemoizeOptions:d=[],devModeChecks:y={}}=a,Q=O(f),Z=O(d),q=K(s),P=m(function(){return o++,u.apply(null,arguments)},...Q),Me=!0,ee=p(function(){i++;let ne=W(q,arguments);return r=P.apply(null,ne),r},...Z);return Object.assign(ee,{resultFunc:u,memoizedResultFunc:P,dependencies:q,dependencyRecomputations:()=>i,resetDependencyRecomputations:()=>{i=0},lastResult:()=>r,recomputations:()=>o,resetRecomputations:()=>{o=0},memoize:m,argsMemoize:p})}}var U=J(R);var ye=(e,t=U)=>{V(e,`createStructuredSelector expects first argument to be an object where each property is a selector, instead received a ${typeof e}`);let n=Object.keys(e),c=n.map(o=>e[o]);return t(c,(...o)=>o.reduce((i,r,l)=>(i[n[l]]=r,i),{}))};export{U as createSelector,J as createSelectorCreator,ye as createStructuredSelector,pe as lruMemoize,pe as weakMapMemoize,x as referenceEqualityCheck,re as setGlobalDevModeChecks,me as unstable_autotrackMemoize}; + //# sourceMappingURL=reselect.browser.mjs.map +\ No newline at end of file +diff --git a/dist/reselect.legacy-esm.js b/dist/reselect.legacy-esm.js +index 9c18982dd0756ccc240f23383b50b893415ba7b3..041426d1db1d1e78cfe35c4e55e38724b2db35dc 100644 +--- a/dist/reselect.legacy-esm.js ++++ b/dist/reselect.legacy-esm.js +@@ -625,6 +625,8 @@ function weakMapMemoize(func, options = {}) { + return memoized; + } + ++weakMapMemoize = lruMemoize ++ + // src/createSelectorCreator.ts + function createSelectorCreator(memoizeOrOptions, ...memoizeOptionsFromArgs) { + const createSelectorCreatorOptions = typeof memoizeOrOptions === "function" ? { +diff --git a/dist/reselect.mjs b/dist/reselect.mjs +index 531dfe6fc16e83dd27dbe90086b5aafea76adb9e..c27aca00d581919325cc595cfa3021cd53c1fa68 100644 +--- a/dist/reselect.mjs ++++ b/dist/reselect.mjs +@@ -606,6 +606,8 @@ function weakMapMemoize(func, options = {}) { + return memoized; + } + ++weakMapMemoize = lruMemoize ++ + // src/createSelectorCreator.ts + function createSelectorCreator(memoizeOrOptions, ...memoizeOptionsFromArgs) { + const createSelectorCreatorOptions = typeof memoizeOrOptions === "function" ? { +diff --git a/src/weakMapMemoize.ts b/src/weakMapMemoize.ts +index f723071db3a8a17f94431bc77cde2dbee026f57f..ddfeb0d7720e5463041d1474f54e58fdbc18fe6d 100644 +--- a/src/weakMapMemoize.ts ++++ b/src/weakMapMemoize.ts +@@ -1,6 +1,7 @@ + // Original source: + // - https://github.com/facebook/react/blob/0b974418c9a56f6c560298560265dcf4b65784bc/packages/react/src/ReactCache.js + ++import { lruMemoize } from '../dist/reselect.mjs' + import type { + AnyFunction, + DefaultMemoizeFields, +@@ -169,97 +170,99 @@ export interface WeakMapMemoizeOptions { + * @public + * @experimental + */ +-export function weakMapMemoize( +- func: Func, +- options: WeakMapMemoizeOptions> = {} +-) { +- let fnNode = createCacheNode() +- const { resultEqualityCheck } = options ++// export function weakMapMemoize( ++// func: Func, ++// options: WeakMapMemoizeOptions> = {} ++// ) { ++// let fnNode = createCacheNode() ++// const { resultEqualityCheck } = options + +- let lastResult: WeakRef | undefined ++// let lastResult: WeakRef | undefined + +- let resultsCount = 0 ++// let resultsCount = 0 + +- function memoized() { +- let cacheNode = fnNode +- const { length } = arguments +- for (let i = 0, l = length; i < l; i++) { +- const arg = arguments[i] +- if ( +- typeof arg === 'function' || +- (typeof arg === 'object' && arg !== null) +- ) { +- // Objects go into a WeakMap +- let objectCache = cacheNode.o +- if (objectCache === null) { +- cacheNode.o = objectCache = new WeakMap() +- } +- const objectNode = objectCache.get(arg) +- if (objectNode === undefined) { +- cacheNode = createCacheNode() +- objectCache.set(arg, cacheNode) +- } else { +- cacheNode = objectNode +- } +- } else { +- // Primitives go into a regular Map +- let primitiveCache = cacheNode.p +- if (primitiveCache === null) { +- cacheNode.p = primitiveCache = new Map() +- } +- const primitiveNode = primitiveCache.get(arg) +- if (primitiveNode === undefined) { +- cacheNode = createCacheNode() +- primitiveCache.set(arg, cacheNode) +- } else { +- cacheNode = primitiveNode +- } +- } +- } ++// function memoized() { ++// let cacheNode = fnNode ++// const { length } = arguments ++// for (let i = 0, l = length; i < l; i++) { ++// const arg = arguments[i] ++// if ( ++// typeof arg === 'function' || ++// (typeof arg === 'object' && arg !== null) ++// ) { ++// // Objects go into a WeakMap ++// let objectCache = cacheNode.o ++// if (objectCache === null) { ++// cacheNode.o = objectCache = new WeakMap() ++// } ++// const objectNode = objectCache.get(arg) ++// if (objectNode === undefined) { ++// cacheNode = createCacheNode() ++// objectCache.set(arg, cacheNode) ++// } else { ++// cacheNode = objectNode ++// } ++// } else { ++// // Primitives go into a regular Map ++// let primitiveCache = cacheNode.p ++// if (primitiveCache === null) { ++// cacheNode.p = primitiveCache = new Map() ++// } ++// const primitiveNode = primitiveCache.get(arg) ++// if (primitiveNode === undefined) { ++// cacheNode = createCacheNode() ++// primitiveCache.set(arg, cacheNode) ++// } else { ++// cacheNode = primitiveNode ++// } ++// } ++// } + +- const terminatedNode = cacheNode as unknown as TerminatedCacheNode ++// const terminatedNode = cacheNode as unknown as TerminatedCacheNode + +- let result ++// let result + +- if (cacheNode.s === TERMINATED) { +- result = cacheNode.v +- } else { +- // Allow errors to propagate +- result = func.apply(null, arguments as unknown as any[]) +- resultsCount++ +- } ++// if (cacheNode.s === TERMINATED) { ++// result = cacheNode.v ++// } else { ++// // Allow errors to propagate ++// result = func.apply(null, arguments as unknown as any[]) ++// resultsCount++ ++// } + +- terminatedNode.s = TERMINATED ++// terminatedNode.s = TERMINATED + +- if (resultEqualityCheck) { +- const lastResultValue = lastResult?.deref() ?? lastResult +- if ( +- lastResultValue != null && +- resultEqualityCheck(lastResultValue as ReturnType, result) +- ) { +- result = lastResultValue +- resultsCount !== 0 && resultsCount-- +- } ++// if (resultEqualityCheck) { ++// const lastResultValue = lastResult?.deref() ?? lastResult ++// if ( ++// lastResultValue != null && ++// resultEqualityCheck(lastResultValue as ReturnType, result) ++// ) { ++// result = lastResultValue ++// resultsCount !== 0 && resultsCount-- ++// } + +- const needsWeakRef = +- (typeof result === 'object' && result !== null) || +- typeof result === 'function' +- lastResult = needsWeakRef ? new Ref(result) : result +- } +- terminatedNode.v = result +- return result +- } ++// const needsWeakRef = ++// (typeof result === 'object' && result !== null) || ++// typeof result === 'function' ++// lastResult = needsWeakRef ? new Ref(result) : result ++// } ++// terminatedNode.v = result ++// return result ++// } + +- memoized.clearCache = () => { +- fnNode = createCacheNode() +- memoized.resetResultsCount() +- } ++// memoized.clearCache = () => { ++// fnNode = createCacheNode() ++// memoized.resetResultsCount() ++// } + +- memoized.resultsCount = () => resultsCount ++// memoized.resultsCount = () => resultsCount + +- memoized.resetResultsCount = () => { +- resultsCount = 0 +- } ++// memoized.resetResultsCount = () => { ++// resultsCount = 0 ++// } + +- return memoized as Func & Simplify +-} ++// return memoized as Func & Simplify ++// } ++ ++export const weakMapMemoize = lruMemoize diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml new file mode 100644 index 00000000000..6a2ed95ab06 --- /dev/null +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -0,0 +1,8775 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@atlaskit/pragmatic-drag-and-drop': + specifier: ^1.7.4 + version: 1.7.4 + '@atlaskit/pragmatic-drag-and-drop-auto-scroll': + specifier: ^2.1.1 + version: 2.1.1 + '@atlaskit/pragmatic-drag-and-drop-hitbox': + specifier: ^1.1.0 + version: 1.1.0 + '@dagrejs/dagre': + specifier: ^1.1.5 + version: 1.1.5 + '@dagrejs/graphlib': + specifier: ^2.2.4 + version: 2.2.4 + '@fontsource-variable/inter': + specifier: ^5.2.6 + version: 5.2.6 + '@invoke-ai/ui-library': + specifier: github:invoke-ai/ui-library#v0.0.48 + version: https://codeload.github.com/invoke-ai/ui-library/tar.gz/c8a1d9a6867cd05ac831b074876c8dca3b0b670d(@chakra-ui/system@2.6.2(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(react@18.3.1))(@fontsource-variable/inter@5.2.6)(@types/react@18.3.23)(i18next@25.3.2(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@nanostores/react': + specifier: ^1.0.0 + version: 1.0.0(nanostores@1.0.1)(react@18.3.1) + '@observ33r/object-equals': + specifier: ^1.1.5 + version: 1.1.5 + '@reduxjs/toolkit': + specifier: 2.8.2 + version: 2.8.2(react-redux@9.2.0(@types/react@18.3.23)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + '@roarr/browser-log-writer': + specifier: ^1.3.0 + version: 1.3.0 + '@xyflow/react': + specifier: ^12.8.2 + version: 12.8.2(@types/react@18.3.23)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + ag-psd: + specifier: ^28.2.2 + version: 28.2.2 + async-mutex: + specifier: ^0.5.0 + version: 0.5.0 + chakra-react-select: + specifier: ^4.9.2 + version: 4.10.1(@chakra-ui/react@2.10.9(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(framer-motion@10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + cmdk: + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + compare-versions: + specifier: ^6.1.1 + version: 6.1.1 + dockview: + specifier: ^4.7.1 + version: 4.7.1(react@18.3.1) + es-toolkit: + specifier: ^1.39.7 + version: 1.39.7 + filesize: + specifier: ^10.1.6 + version: 10.1.6 + fracturedjsonjs: + specifier: ^4.1.0 + version: 4.1.0 + framer-motion: + specifier: ^11.10.0 + version: 11.18.2(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + i18next: + specifier: ^25.3.2 + version: 25.3.2(typescript@5.8.3) + i18next-http-backend: + specifier: ^3.0.2 + version: 3.0.2 + idb-keyval: + specifier: 6.2.1 + version: 6.2.1 + jsondiffpatch: + specifier: ^0.7.3 + version: 0.7.3 + jszip: + specifier: ^3.10.1 + version: 3.10.1 + konva: + specifier: ^9.3.22 + version: 9.3.22 + linkify-react: + specifier: ^4.3.1 + version: 4.3.1(linkifyjs@4.3.1)(react@18.3.1) + linkifyjs: + specifier: ^4.3.1 + version: 4.3.1 + lru-cache: + specifier: ^11.1.0 + version: 11.1.0 + mtwist: + specifier: ^1.0.2 + version: 1.0.2 + nanoid: + specifier: ^5.1.5 + version: 5.1.5 + nanostores: + specifier: ^1.0.1 + version: 1.0.1 + new-github-issue-url: + specifier: ^1.1.0 + version: 1.1.0 + overlayscrollbars: + specifier: ^2.11.4 + version: 2.11.4 + overlayscrollbars-react: + specifier: ^0.5.6 + version: 0.5.6(overlayscrollbars@2.11.4)(react@18.3.1) + perfect-freehand: + specifier: ^1.2.2 + version: 1.2.2 + query-string: + specifier: ^9.2.1 + version: 9.2.2 + raf-throttle: + specifier: ^2.0.6 + version: 2.0.6 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-colorful: + specifier: ^5.6.1 + version: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-dropzone: + specifier: ^14.3.8 + version: 14.3.8(react@18.3.1) + react-error-boundary: + specifier: ^5.0.0 + version: 5.0.0(react@18.3.1) + react-hook-form: + specifier: ^7.60.0 + version: 7.60.0(react@18.3.1) + react-hotkeys-hook: + specifier: 4.5.0 + version: 4.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-i18next: + specifier: ^15.5.3 + version: 15.6.0(i18next@25.3.2(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@18.3.1) + react-redux: + specifier: 9.2.0 + version: 9.2.0(@types/react@18.3.23)(react@18.3.1)(redux@5.0.1) + react-resizable-panels: + specifier: ^3.0.3 + version: 3.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router-dom: + specifier: ^7.12.0 + version: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-textarea-autosize: + specifier: ^8.5.9 + version: 8.5.9(@types/react@18.3.23)(react@18.3.1) + react-use: + specifier: ^17.6.0 + version: 17.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-virtuoso: + specifier: ^4.13.0 + version: 4.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + redux-remember: + specifier: ^5.2.0 + version: 5.2.0(redux@5.0.1) + redux-undo: + specifier: ^1.1.0 + version: 1.1.0 + rfdc: + specifier: ^1.4.1 + version: 1.4.1 + roarr: + specifier: ^7.21.1 + version: 7.21.1 + serialize-error: + specifier: ^12.0.0 + version: 12.0.0 + socket.io-client: + specifier: ^4.8.1 + version: 4.8.1 + stable-hash: + specifier: ^0.0.6 + version: 0.0.6 + use-debounce: + specifier: ^10.0.5 + version: 10.0.5(react@18.3.1) + use-device-pixel-ratio: + specifier: ^1.1.2 + version: 1.1.2(react@18.3.1) + uuid: + specifier: ^11.1.0 + version: 11.1.0 + zod: + specifier: ^4.0.10 + version: 4.0.10 + zod-validation-error: + specifier: ^3.5.2 + version: 3.5.3(zod@4.0.10) + devDependencies: + '@eslint/js': + specifier: ^9.31.0 + version: 9.31.0 + '@storybook/addon-docs': + specifier: ^9.0.17 + version: 9.0.17(@types/react@18.3.23)(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2)) + '@storybook/addon-links': + specifier: ^9.0.17 + version: 9.0.17(react@18.3.1)(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2)) + '@storybook/react-vite': + specifier: ^9.0.17 + version: 9.0.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3)(vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2)) + '@types/node': + specifier: ^22.15.1 + version: 22.16.0 + '@types/react': + specifier: ^18.3.11 + version: 18.3.23 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.23) + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.37.0 + version: 8.37.0(@typescript-eslint/parser@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^8.37.0 + version: 8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + '@vitejs/plugin-react-swc': + specifier: ^3.9.0 + version: 3.10.2(vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2)) + '@vitest/coverage-v8': + specifier: ^3.1.2 + version: 3.2.4(vitest@3.2.4) + '@vitest/ui': + specifier: ^3.1.2 + version: 3.2.4(vitest@3.2.4) + concurrently: + specifier: ^9.1.2 + version: 9.2.0 + csstype: + specifier: ^3.1.3 + version: 3.1.3 + dpdm: + specifier: ^3.14.0 + version: 3.14.0 + eslint: + specifier: ^9.31.0 + version: 9.31.0(jiti@2.4.2) + eslint-plugin-i18next: + specifier: ^6.1.2 + version: 6.1.2 + eslint-plugin-import: + specifier: ^2.29.1 + version: 2.32.0(@typescript-eslint/parser@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2)) + eslint-plugin-path: + specifier: ^2.0.3 + version: 2.0.3(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + eslint-plugin-react: + specifier: ^7.33.2 + version: 7.37.5(eslint@9.31.0(jiti@2.4.2)) + eslint-plugin-react-hooks: + specifier: ^5.2.0 + version: 5.2.0(eslint@9.31.0(jiti@2.4.2)) + eslint-plugin-react-refresh: + specifier: ^0.4.5 + version: 0.4.20(eslint@9.31.0(jiti@2.4.2)) + eslint-plugin-simple-import-sort: + specifier: ^12.0.0 + version: 12.1.1(eslint@9.31.0(jiti@2.4.2)) + eslint-plugin-storybook: + specifier: ^9.0.17 + version: 9.0.17(eslint@9.31.0(jiti@2.4.2))(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3) + eslint-plugin-unused-imports: + specifier: ^4.1.4 + version: 4.1.4(@typescript-eslint/eslint-plugin@8.37.0(@typescript-eslint/parser@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2)) + globals: + specifier: ^16.3.0 + version: 16.3.0 + knip: + specifier: ^5.61.3 + version: 5.61.3(@types/node@22.16.0)(typescript@5.8.3) + magic-string: + specifier: ^0.30.17 + version: 0.30.17 + openapi-types: + specifier: ^12.1.3 + version: 12.1.3 + openapi-typescript: + specifier: ^7.6.1 + version: 7.8.0(typescript@5.8.3) + prettier: + specifier: ^3.5.3 + version: 3.6.2 + rollup-plugin-visualizer: + specifier: ^6.0.3 + version: 6.0.3(rollup@4.45.1) + storybook: + specifier: ^9.0.17 + version: 9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2) + tsafe: + specifier: ^1.8.5 + version: 1.8.5 + type-fest: + specifier: ^4.40.0 + version: 4.41.0 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vite: + specifier: ^7.0.5 + version: 7.0.5(@types/node@22.16.0)(jiti@2.4.2) + vite-plugin-eslint: + specifier: ^1.8.1 + version: 1.8.1(eslint@9.31.0(jiti@2.4.2))(vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2)) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.8.3)(vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2)) + vitest: + specifier: ^3.1.2 + version: 3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jiti@2.4.2) + +packages: + + '@adobe/css-tools@4.4.3': + resolution: {integrity: sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.1': + resolution: {integrity: sha512-VAQEb3NVLY9Q5ZgC5Eiws9Uf6xOINY9/pAZMdbOVlF90uRXEkmpYqdTL+zeyZ8U8deuqYCmXr7oWIEnxpNQVzA==} + + '@atlaskit/pragmatic-drag-and-drop-hitbox@1.1.0': + resolution: {integrity: sha512-JWt6eVp6Br2FPHRM8s0dUIHQk/jFInGP1f3ti5CdtM1Ji5/pt8Akm44wDC063Gv2i5RGseixtbW0z/t6RYtbdg==} + + '@atlaskit/pragmatic-drag-and-drop@1.7.4': + resolution: {integrity: sha512-lZHnO9BJdHPKnwB0uvVUCyDnIhL+WAHzXQ2EXX0qacogOsnvIUiCgY0BLKhBqTCWln3/f/Ox5jU54MKO6ayh9A==} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.0': + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.0': + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.6': + resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.27.6': + resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.28.3': + resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.0': + resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.0': + resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.1': + resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@chakra-ui/anatomy@2.2.2': + resolution: {integrity: sha512-MV6D4VLRIHr4PkW4zMyqfrNS1mPlCTiCXwvYGtDFQYr+xHFfonhAuf9WjsSc0nyp2m0OdkSLnzmVKkZFLo25Tg==} + + '@chakra-ui/anatomy@2.3.4': + resolution: {integrity: sha512-fFIYN7L276gw0Q7/ikMMlZxP7mvnjRaWJ7f3Jsf9VtDOi6eAYIBRrhQe6+SZ0PGmoOkRaBc7gSE5oeIbgFFyrw==} + + '@chakra-ui/anatomy@2.3.6': + resolution: {integrity: sha512-TjmjyQouIZzha/l8JxdBZN1pKZTj7sLpJ0YkFnQFyqHcbfWggW9jKWzY1E0VBnhtFz/xF3KC6UAVuZVSJx+y0g==} + + '@chakra-ui/breakpoint-utils@2.0.8': + resolution: {integrity: sha512-Pq32MlEX9fwb5j5xx8s18zJMARNHlQZH2VH1RZgfgRDpp7DcEgtRW5AInfN5CfqdHLO1dGxA7I3MqEuL5JnIsA==} + + '@chakra-ui/color-mode@2.2.0': + resolution: {integrity: sha512-niTEA8PALtMWRI9wJ4LL0CSBDo8NBfLNp4GD6/0hstcm3IlbBHTVKxN6HwSaoNYfphDQLxCjT4yG+0BJA5tFpg==} + peerDependencies: + react: '>=18' + + '@chakra-ui/hooks@2.4.5': + resolution: {integrity: sha512-601fWfHE2i7UjaxK/9lDLlOni6vk/I+04YDbM0BrelJy+eqxdlOmoN8Z6MZ3PzFh7ofERUASor+vL+/HaCaZ7w==} + peerDependencies: + react: '>=18' + + '@chakra-ui/icon@3.2.0': + resolution: {integrity: sha512-xxjGLvlX2Ys4H0iHrI16t74rG9EBcpFvJ3Y3B7KMQTrnW34Kf7Da/UC8J67Gtx85mTHW020ml85SVPKORWNNKQ==} + peerDependencies: + '@chakra-ui/system': '>=2.0.0' + react: '>=18' + + '@chakra-ui/icons@2.2.4': + resolution: {integrity: sha512-l5QdBgwrAg3Sc2BRqtNkJpfuLw/pWRDwwT58J6c4PqQT6wzXxyNa8Q0PForu1ltB5qEiFb1kxr/F/HO1EwNa6g==} + peerDependencies: + '@chakra-ui/react': '>=2.0.0' + react: '>=18' + + '@chakra-ui/layout@2.3.1': + resolution: {integrity: sha512-nXuZ6WRbq0WdgnRgLw+QuxWAHuhDtVX8ElWqcTK+cSMFg/52eVP47czYBE5F35YhnoW2XBwfNoNgZ7+e8Z01Rg==} + peerDependencies: + '@chakra-ui/system': '>=2.0.0' + react: '>=18' + + '@chakra-ui/object-utils@2.1.0': + resolution: {integrity: sha512-tgIZOgLHaoti5PYGPTwK3t/cqtcycW0owaiOXoZOcpwwX/vlVb+H1jFsQyWiiwQVPt9RkoSLtxzXamx+aHH+bQ==} + + '@chakra-ui/portal@2.1.0': + resolution: {integrity: sha512-9q9KWf6SArEcIq1gGofNcFPSWEyl+MfJjEUg/un1SMlQjaROOh3zYr+6JAwvcORiX7tyHosnmWC3d3wI2aPSQg==} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + '@chakra-ui/react-children-utils@2.0.6': + resolution: {integrity: sha512-QVR2RC7QsOsbWwEnq9YduhpqSFnZGvjjGREV8ygKi8ADhXh93C8azLECCUVgRJF2Wc+So1fgxmjLcbZfY2VmBA==} + peerDependencies: + react: '>=18' + + '@chakra-ui/react-context@2.1.0': + resolution: {integrity: sha512-iahyStvzQ4AOwKwdPReLGfDesGG+vWJfEsn0X/NoGph/SkN+HXtv2sCfYFFR9k7bb+Kvc6YfpLlSuLvKMHi2+w==} + peerDependencies: + react: '>=18' + + '@chakra-ui/react-use-safe-layout-effect@2.1.0': + resolution: {integrity: sha512-Knbrrx/bcPwVS1TorFdzrK/zWA8yuU/eaXDkNj24IrKoRlQrSBFarcgAEzlCHtzuhufP3OULPkELTzz91b0tCw==} + peerDependencies: + react: '>=18' + + '@chakra-ui/react-utils@2.0.12': + resolution: {integrity: sha512-GbSfVb283+YA3kA8w8xWmzbjNWk14uhNpntnipHCftBibl0lxtQ9YqMFQLwuFOO0U2gYVocszqqDWX+XNKq9hw==} + peerDependencies: + react: '>=18' + + '@chakra-ui/react@2.10.9': + resolution: {integrity: sha512-lhdcgoocOiURwBNR3L8OioCNIaGCZqRfuKioLyaQLjOanl4jr0PQclsGb+w0cmito252vEWpsz2xRqF7y+Flrw==} + peerDependencies: + '@emotion/react': '>=11' + '@emotion/styled': '>=11' + framer-motion: '>=4.0.0' + react: '>=18' + react-dom: '>=18' + + '@chakra-ui/shared-utils@2.0.5': + resolution: {integrity: sha512-4/Wur0FqDov7Y0nCXl7HbHzCg4aq86h+SXdoUeuCMD3dSj7dpsVnStLYhng1vxvlbUnLpdF4oz5Myt3i/a7N3Q==} + + '@chakra-ui/styled-system@2.12.0': + resolution: {integrity: sha512-zoqLw1I2y4GlZ0LDoyw8o0JjoDOW6u0IwFPAoHuw0UMbP8glHUGvwEL1STug/i/GzBKw83yoF6ae41HIQvhMww==} + + '@chakra-ui/styled-system@2.12.4': + resolution: {integrity: sha512-oa07UG7Lic5hHSQtGRiMEnYjuhIa8lszyuVhZjZqR2Ap3VMF688y1MVPJ1pK+8OwY5uhXBgVd5c0+rI8aBZlwg==} + + '@chakra-ui/styled-system@2.9.2': + resolution: {integrity: sha512-To/Z92oHpIE+4nk11uVMWqo2GGRS86coeMmjxtpnErmWRdLcp1WVCVRAvn+ZwpLiNR+reWFr2FFqJRsREuZdAg==} + + '@chakra-ui/system@2.6.2': + resolution: {integrity: sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==} + peerDependencies: + '@emotion/react': ^11.0.0 + '@emotion/styled': ^11.0.0 + react: '>=18' + + '@chakra-ui/theme-tools@2.1.2': + resolution: {integrity: sha512-Qdj8ajF9kxY4gLrq7gA+Azp8CtFHGO9tWMN2wfF9aQNgG9AuMhPrUzMq9AMQ0MXiYcgNq/FD3eegB43nHVmXVA==} + peerDependencies: + '@chakra-ui/styled-system': '>=2.0.0' + + '@chakra-ui/theme-tools@2.2.6': + resolution: {integrity: sha512-3UhKPyzKbV3l/bg1iQN9PBvffYp+EBOoYMUaeTUdieQRPFzo2jbYR0lNCxqv8h5aGM/k54nCHU2M/GStyi9F2A==} + peerDependencies: + '@chakra-ui/styled-system': '>=2.0.0' + + '@chakra-ui/theme-tools@2.2.9': + resolution: {integrity: sha512-PcbYL19lrVvEc7Oydy//jsy/MO/rZz1DvLyO6AoI+bI/+Kwz9WfOKsspbulEhRg5COayE0R/IZPsskXZ7Mp4bA==} + peerDependencies: + '@chakra-ui/styled-system': '>=2.0.0' + + '@chakra-ui/theme-utils@2.0.21': + resolution: {integrity: sha512-FjH5LJbT794r0+VSCXB3lT4aubI24bLLRWB+CuRKHijRvsOg717bRdUN/N1fEmEpFnRVrbewttWh/OQs0EWpWw==} + + '@chakra-ui/theme@3.3.1': + resolution: {integrity: sha512-Hft/VaT8GYnItGCBbgWd75ICrIrIFrR7lVOhV/dQnqtfGqsVDlrztbSErvMkoPKt0UgAkd9/o44jmZ6X4U2nZQ==} + peerDependencies: + '@chakra-ui/styled-system': '>=2.8.0' + + '@chakra-ui/theme@3.4.9': + resolution: {integrity: sha512-GAom2SjSdRWTcX76/2yJOFJsOWHQeBgaynCUNBsHq62OafzvELrsSHDUw0bBqBb1c2ww0CclIvGilPup8kXBFA==} + peerDependencies: + '@chakra-ui/styled-system': '>=2.8.0' + + '@chakra-ui/utils@2.0.15': + resolution: {integrity: sha512-El4+jL0WSaYYs+rJbuYFDbjmfCcfGDmRY95GO4xwzit6YAPZBLcR65rOEwLps+XWluZTy1xdMrusg/hW0c1aAA==} + + '@chakra-ui/utils@2.2.2': + resolution: {integrity: sha512-jUPLT0JzRMWxpdzH6c+t0YMJYrvc5CLericgITV3zDSXblkfx3DsYXqU11DJTSGZI9dUKzM1Wd0Wswn4eJwvFQ==} + peerDependencies: + react: '>=16.8.0' + + '@chakra-ui/utils@2.2.5': + resolution: {integrity: sha512-KTBCK+M5KtXH6p54XS39ImQUMVtAx65BoZDoEms3LuObyTo1+civ1sMm4h3nRT320U6H5H7D35WnABVQjqU/4g==} + peerDependencies: + react: '>=16.8.0' + + '@dagrejs/dagre@1.1.5': + resolution: {integrity: sha512-Ghgrh08s12DCL5SeiR6AoyE80mQELTWhJBRmXfFoqDiFkR458vPEdgTbbjA0T+9ETNxUblnD0QW55tfdvi5pjQ==} + + '@dagrejs/graphlib@2.2.4': + resolution: {integrity: sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==} + engines: {node: '>17.0.0'} + + '@dmsnell/diff-match-patch@1.1.0': + resolution: {integrity: sha512-yejLPmM5pjsGvxS9gXablUSbInW7H976c/FJ4iQxWIm7/38xBySRemTPDe34lhg1gVLbJntX0+sH0jYfU+PN9A==} + + '@emnapi/core@1.4.3': + resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} + + '@emnapi/runtime@1.4.3': + resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} + + '@emnapi/wasi-threads@1.0.2': + resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==} + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/is-prop-valid@0.8.8': + resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} + + '@emotion/is-prop-valid@1.3.1': + resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==} + + '@emotion/memoize@0.7.4': + resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/styled@11.14.1': + resolution: {integrity: sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@esbuild/aix-ppc64@0.25.6': + resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.6': + resolution: {integrity: sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.6': + resolution: {integrity: sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.6': + resolution: {integrity: sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.6': + resolution: {integrity: sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.6': + resolution: {integrity: sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.6': + resolution: {integrity: sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.6': + resolution: {integrity: sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.6': + resolution: {integrity: sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.6': + resolution: {integrity: sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.6': + resolution: {integrity: sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.6': + resolution: {integrity: sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.6': + resolution: {integrity: sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.6': + resolution: {integrity: sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.6': + resolution: {integrity: sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.6': + resolution: {integrity: sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.6': + resolution: {integrity: sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.6': + resolution: {integrity: sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.6': + resolution: {integrity: sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.6': + resolution: {integrity: sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.6': + resolution: {integrity: sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.6': + resolution: {integrity: sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.6': + resolution: {integrity: sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.6': + resolution: {integrity: sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.6': + resolution: {integrity: sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.6': + resolution: {integrity: sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.0': + resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.1': + resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.31.0': + resolution: {integrity: sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.3': + resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.2': + resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==} + + '@floating-ui/dom@1.7.2': + resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@fontsource-variable/inter@5.2.6': + resolution: {integrity: sha512-jks/bficUPQ9nn7GvXvHtlQIPudW7Wx8CrlZoY8bhxgeobNxlQan8DclUJuYF2loYRrGpfrhCIZZspXYysiVGg==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@invoke-ai/ui-library@https://codeload.github.com/invoke-ai/ui-library/tar.gz/c8a1d9a6867cd05ac831b074876c8dca3b0b670d': + resolution: {tarball: https://codeload.github.com/invoke-ai/ui-library/tar.gz/c8a1d9a6867cd05ac831b074876c8dca3b0b670d} + version: 0.0.48 + peerDependencies: + '@fontsource-variable/inter': ^5.0.16 + react: ^18.2.0 + react-dom: ^18.2.0 + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1': + resolution: {integrity: sha512-J4BaTocTOYFkMHIra1JDWrMWpNmBl4EkplIwHEsV8aeUOtdWjwSnln9U7twjMFTAEB7mptNtSKyVi1Y2W9sDJw==} + peerDependencies: + typescript: '>= 4.3.x' + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + + '@mdx-js/react@3.1.0': + resolution: {integrity: sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + '@nanostores/react@1.0.0': + resolution: {integrity: sha512-eDduyNy+lbQJMg6XxZ/YssQqF6b4OXMFEZMYKPJCCmBevp1lg0g+4ZRi94qGHirMtsNfAWKNwsjOhC+q1gvC+A==} + engines: {node: ^20.0.0 || >=22.0.0} + peerDependencies: + nanostores: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^1.0.0 + react: '>=18.0.0' + + '@napi-rs/wasm-runtime@0.2.11': + resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@observ33r/object-equals@1.1.5': + resolution: {integrity: sha512-CVEMmateIaqPIdhKJ2QVf+S1QNypAZPrOAEXqPyC9RCMJ19rPT4A8J1TyzhOk1WAhW04p3hd8JdmNAy1LaVuag==} + + '@oxc-resolver/binding-darwin-arm64@11.4.0': + resolution: {integrity: sha512-+mlX+/yoWv/IfWad97mn/5KVYtwe/VLjwtyoY04UUL+VrHk0MpANAorM9gFf+7K6GkQEaNkTK1g4GqwPI8OiCQ==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.4.0': + resolution: {integrity: sha512-uImuGdgleCPvZFfsYM7WnDW3PZ5z/cwrOt37MFd++rtrQ9kEL32ezl85eyatX2KsvGq7E8qg1DvHLVHOW259uQ==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.4.0': + resolution: {integrity: sha512-/uFsbq+RWHiOktH1c9AysKZ++nHj76+chjQrCIvKalHYHKn6ydhMM6GwHL/pWq/gCZADbiKRQ0AOYLNf86hsZg==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.4.0': + resolution: {integrity: sha512-GPQcVSW2zgc8MtTF5ovIfmXkMCoGATzOmMOinLKjStvqq/KX9tBoVHhR/r7g7ChIJjozeXMMSYrf1q6r3zWXjA==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.4.0': + resolution: {integrity: sha512-rrOFswgslH2i/e2HHP6ei2Z3ivWKvkU666eL1hPXkzHHzhlavIp5vOywjlNR7fZK/15PG4/GKcGsHAVunHd/+w==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-musl@11.4.0': + resolution: {integrity: sha512-+OQ0rckRSYNP3wuEw+Asf5Is0elLeHkmhzlRAjx20lkITgSaNtkk7wDaqlpJmkcPv6ja3YkOoMiyclfS/FMSGA==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.4.0': + resolution: {integrity: sha512-hOmV2yNr4y5BVDaXPl3aCZASBsVLo4eAd7UWfItG2l1CMcZdtE35XIo0dB3xUg1DGDI5n02eo89014GN246aHA==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-s390x-gnu@11.4.0': + resolution: {integrity: sha512-STBciuunyjnQAhaJQoOEON3uQWL/Ad7mL+Ap8Q9A2Zw2bxZR7iW+tMu8pJDljHGVtGxtP1uurUt68kY9bMkFhA==} + cpu: [s390x] + os: [linux] + + '@oxc-resolver/binding-linux-x64-gnu@11.4.0': + resolution: {integrity: sha512-x9uFAdBZ2SfVSWcQxza3GHo/5enZWLWb8Nf6zBCu0eBee/IL/z5oJIGpF/9xFwlvT4k35ZYHxBC33NGB4SkkGw==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-linux-x64-musl@11.4.0': + resolution: {integrity: sha512-DrPkwPdufbka98aVvJP+qC29LP1MltUm6KPH0sJ5v9g5Tj+qcLi8i1EG5n8fnIqOI3vMtYs3DS2yMR2UGF7xyw==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-wasm32-wasi@11.4.0': + resolution: {integrity: sha512-fRBFgjhiUWTfz/7H/98r6SHsqCu3FvQPxbbDAs0wEVRvQdu7rZ2Ur2i4vKCZ6qLx6mDiBUKrkXy0btmU7eSrkQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.4.0': + resolution: {integrity: sha512-Vl32WwWT6aVk0qjfmXRH1BYwtLh8UHEanuuaNyAU4i/I9+Qx8SvNRNo39sRl1g7pHDcdeUgqFoNZkVXwIC5xVw==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.4.0': + resolution: {integrity: sha512-bBvYlfLTV4uH5pXLnNlx4BZ9DAsV3yQHL3vhXE2PfQ+iZglBkSZU/f82hx8cNwewTaK08zJUz4m2vGMQiSyU8Q==} + cpu: [x64] + os: [win32] + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@radix-ui/primitive@1.1.2': + resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.14': + resolution: {integrity: sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.10': + resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.2': + resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.4': + resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.22.2': + resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==} + + '@redocly/openapi-core@1.34.3': + resolution: {integrity: sha512-3arRdUp1fNx55itnjKiUhO6t4Mf91TsrTIYINDNLAZPS0TPd5YpiXRctwjel0qqWoOOhjA34cZ3m4dksLDFUYg==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} + + '@reduxjs/toolkit@2.8.2': + resolution: {integrity: sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + + '@roarr/browser-log-writer@1.3.0': + resolution: {integrity: sha512-RTzjxrm0CpTSoESmsO6104VymAksDS/yJEkaZrL/OLfbM6q+J+jLRBLtJxhJHSY03pBWOEE3wRh+pVwfKtBPqg==} + engines: {node: '>=12.0'} + + '@rolldown/pluginutils@1.0.0-beta.11': + resolution: {integrity: sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==} + + '@rollup/pluginutils@4.2.1': + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + + '@rollup/pluginutils@5.2.0': + resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} + 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.45.1': + resolution: {integrity: sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.45.1': + resolution: {integrity: sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.45.1': + resolution: {integrity: sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.45.1': + resolution: {integrity: sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.45.1': + resolution: {integrity: sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.45.1': + resolution: {integrity: sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.45.1': + resolution: {integrity: sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.45.1': + resolution: {integrity: sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.45.1': + resolution: {integrity: sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.45.1': + resolution: {integrity: sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.45.1': + resolution: {integrity: sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': + resolution: {integrity: sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.45.1': + resolution: {integrity: sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.45.1': + resolution: {integrity: sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.45.1': + resolution: {integrity: sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.45.1': + resolution: {integrity: sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.45.1': + resolution: {integrity: sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.45.1': + resolution: {integrity: sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.45.1': + resolution: {integrity: sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.45.1': + resolution: {integrity: sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@storybook/addon-docs@9.0.17': + resolution: {integrity: sha512-LOX/kKgQGnyulrqZHsvf77+ZoH/nSUaplGr5hvZglW/U6ak6fO9seJyXAzVKEnC6p+F8n02kFBZbi3s+znQhSg==} + peerDependencies: + storybook: ^9.0.17 + + '@storybook/addon-links@9.0.17': + resolution: {integrity: sha512-c4hYojq0O6n5fD8MS+Ss1njR3qs88LLlO3LLaRD4bxsIgn8WFNjgG5677M7m8WjzTgWSxFWN0KAra2kaDZ8Jlg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.0.17 + peerDependenciesMeta: + react: + optional: true + + '@storybook/builder-vite@9.0.17': + resolution: {integrity: sha512-lyuvgGhb0NaVk1tdB4xwzky6+YXQfxlxfNQqENYZ9uYQZdPfErMa4ZTXVQTV+CQHAa2NL+p/dG2JPAeu39e9UA==} + peerDependencies: + storybook: ^9.0.17 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@storybook/csf-plugin@9.0.17': + resolution: {integrity: sha512-6Q4eo1ObrLlsnB6bIt6T8+45XAb4to2pQGNrI7QPkLQRLrZinrJcNbLY7AGkyIoCOEsEbq08n09/nClQUbu8HA==} + peerDependencies: + storybook: ^9.0.17 + + '@storybook/global@5.0.0': + resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} + + '@storybook/icons@1.4.0': + resolution: {integrity: sha512-Td73IeJxOyalzvjQL+JXx72jlIYHgs+REaHiREOqfpo3A2AYYG71AUbcv+lg7mEDIweKVCxsMQ0UKo634c8XeA==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + + '@storybook/react-dom-shim@9.0.17': + resolution: {integrity: sha512-ak/x/m6MDDxdE6rCDymTltaiQF3oiKrPHSwfM+YPgQR6MVmzTTs4+qaPfeev7FZEHq23IkfDMTmSTTJtX7Vs9A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.0.17 + + '@storybook/react-vite@9.0.17': + resolution: {integrity: sha512-wx1yKScni4ifOC/ccqpnnpceQbyF2xto+jHGsyua+M4UUCQdS2NYPDR8JFWp1YvBhVt2cQiD6SAltVGM9QLGnQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.0.17 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@storybook/react@9.0.17': + resolution: {integrity: sha512-wssao+uXg72OHtEJdQmmQJGdX90x/aU/6avoP3fgVgepWdZXVgciS9mnqHjKRF/vP+vPOlNQcJjojF/zTtq5qg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.0.17 + typescript: '>= 4.9.x' + peerDependenciesMeta: + typescript: + optional: true + + '@swc/core-darwin-arm64@1.12.9': + resolution: {integrity: sha512-GACFEp4nD6V+TZNR2JwbMZRHB+Yyvp14FrcmB6UCUYmhuNWjkxi+CLnEvdbuiKyQYv0zA+TRpCHZ+whEs6gwfA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.12.9': + resolution: {integrity: sha512-hv2kls7Ilkm2EpeJz+I9MCil7pGS3z55ZAgZfxklEuYsxpICycxeH+RNRv4EraggN44ms+FWCjtZFu0LGg2V3g==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.12.9': + resolution: {integrity: sha512-od9tDPiG+wMU9wKtd6y3nYJdNqgDOyLdgRRcrj1/hrbHoUPOM8wZQZdwQYGarw63iLXGgsw7t5HAF9Yc51ilFA==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.12.9': + resolution: {integrity: sha512-6qx1ka9LHcLzxIgn2Mros+CZLkHK2TawlXzi/h7DJeNnzi8F1Hw0Yzjp8WimxNCg6s2n+o3jnmin1oXB7gg8rw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.12.9': + resolution: {integrity: sha512-yghFZWKPVVGbUdqiD7ft23G0JX6YFGDJPz9YbLLAwGuKZ9th3/jlWoQDAw1Naci31LQhVC+oIji6ozihSuwB2A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.12.9': + resolution: {integrity: sha512-SFUxyhWLZRNL8QmgGNqdi2Q43PNyFVkRZ2zIif30SOGFSxnxcf2JNeSeBgKIGVgaLSuk6xFVVCtJ3KIeaStgRg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.12.9': + resolution: {integrity: sha512-9FB0wM+6idCGTI20YsBNBg9xSWtkDBymnpaTCsZM3qDc0l4uOpJMqbfWhQvp17x7r/ulZfb2QY8RDvQmCL6AcQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.12.9': + resolution: {integrity: sha512-zHOusMVbOH9ik5RtRrMiGzLpKwxrPXgXkBm3SbUCa65HAdjV33NZ0/R9Rv1uPESALtEl2tzMYLUxYA5ECFDFhA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.12.9': + resolution: {integrity: sha512-aWZf0PqE0ot7tCuhAjRkDFf41AzzSQO0x2xRfTbnhpROp57BRJ/N5eee1VULO/UA2PIJRG7GKQky5bSGBYlFug==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.12.9': + resolution: {integrity: sha512-C25fYftXOras3P3anSUeXXIpxmEkdAcsIL9yrr0j1xepTZ/yKwpnQ6g3coj8UXdeJy4GTVlR6+Ow/QiBgZQNOg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.12.9': + resolution: {integrity: sha512-O+LfT2JlVMsIMWG9x+rdxg8GzpzeGtCZQfXV7cKc1PjIKUkLFf1QJ7okuseA4f/9vncu37dQ2ZcRrPKy0Ndd5g==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.23': + resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==} + + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.6.3': + resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@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/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/doctrine@0.0.9': + resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + + '@types/eslint@8.56.12': + resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/js-cookie@2.2.7': + resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/lodash.mergewith@4.6.7': + resolution: {integrity: sha512-3m+lkO5CLRRYU0fhGRp7zbsGi6+BZj0uTVSwvcKU+nSlhjA9/QRNfuSGnD2mX6hQA7ZbmcCkzk5h4ZYGOtk14A==} + + '@types/lodash.mergewith@4.6.9': + resolution: {integrity: sha512-fgkoCAOF47K7sxrQ7Mlud2TH023itugZs2bUg8h/KzT+BnZNrR2jAOmaokbLunHNnobXVWOezAeNn/lZqwxkcw==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + + '@types/node@22.16.0': + resolution: {integrity: sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + + '@types/react@18.3.23': + resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} + + '@types/resolve@1.20.6': + resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} + + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@typescript-eslint/eslint-plugin@8.37.0': + resolution: {integrity: sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.37.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/parser@8.37.0': + resolution: {integrity: sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/project-service@8.37.0': + resolution: {integrity: sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/scope-manager@8.37.0': + resolution: {integrity: sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.37.0': + resolution: {integrity: sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/type-utils@8.37.0': + resolution: {integrity: sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/types@8.37.0': + resolution: {integrity: sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.37.0': + resolution: {integrity: sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/utils@8.37.0': + resolution: {integrity: sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/visitor-keys@8.37.0': + resolution: {integrity: sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react-swc@3.10.2': + resolution: {integrity: sha512-xD3Rdvrt5LgANug7WekBn1KhcvLn1H3jNBfJRL3reeOIua/WnZOEV5qi5qIBq5T8R0jUDmRtxuvk4bPhzGHDWw==} + peerDependencies: + vite: ^4 || ^5 || ^6 || ^7.0.0-beta.0 + + '@vitest/coverage-v8@3.2.4': + resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} + peerDependencies: + '@vitest/browser': 3.2.4 + vitest: 3.2.4 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/ui@3.2.4': + resolution: {integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==} + peerDependencies: + vitest: 3.2.4 + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + '@xobotyi/scrollbar-width@1.9.5': + resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} + + '@xyflow/react@12.8.2': + resolution: {integrity: sha512-VifLpxOy74ck283NQOtBn1e8igmB7xo7ADDKxyBHkKd8IKpyr16TgaYOhzqVwNMdB4NT+m++zfkic530L+gEXw==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@xyflow/system@0.0.66': + resolution: {integrity: sha512-TTxESDwPsATnuDMUeYYtKe4wt9v8bRO29dgYBhR8HyhSCzipnAdIL/1CDfFd+WqS1srVreo24u6zZeVIDk4r3Q==} + + '@zag-js/dom-query@0.31.1': + resolution: {integrity: sha512-oiuohEXAXhBxpzzNm9k2VHGEOLC1SXlXSbRPcfBZ9so5NRQUA++zCE7cyQJqGLTZR0t3itFLlZqDbYEXRrefwg==} + + '@zag-js/element-size@0.31.1': + resolution: {integrity: sha512-4T3yvn5NqqAjhlP326Fv+w9RqMIBbNN9H72g5q2ohwzhSgSfZzrKtjL4rs9axY/cw9UfMfXjRjEE98e5CMq7WQ==} + + '@zag-js/focus-visible@0.31.1': + resolution: {integrity: sha512-dbLksz7FEwyFoANbpIlNnd3bVm0clQSUsnP8yUVQucStZPsuWjCrhL2jlAbGNrTrahX96ntUMXHb/sM68TibFg==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ag-psd@28.2.2: + resolution: {integrity: sha512-93fuYmO3cvVwS2mEbS2uWtBP+4YuVF+0277e18oSFqM7uWyJWJkEz+J9ZCcyDwbf2NxQf0YdACZKf9d3+jedyg==} + + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + ast-v8-to-istanbul@0.3.3: + resolution: {integrity: sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + async-mutex@0.5.0: + resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + + attr-accept@2.2.5: + resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} + engines: {node: '>=4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + better-opn@3.0.2: + resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} + engines: {node: '>=12.0.0'} + + bind-event-listener@3.0.0: + resolution: {integrity: sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.25.1: + resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001727: + resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} + + chai@5.2.0: + resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} + engines: {node: '>=12'} + + chai@5.2.1: + resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} + engines: {node: '>=18'} + + chakra-react-select@4.10.1: + resolution: {integrity: sha512-0d7lubrmcm7molVYNYWEYi7o71W8wn/WruINon+m23XQLYvJ+bZlYVawDdWYdJjX8O1nzJlTDo4b7CB6zTsr4A==} + peerDependencies: + '@chakra-ui/react': 2.x + '@emotion/react': ^11.8.1 + react: ^18.0.0 + react-dom: ^18.0.0 + + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + classcat@5.0.5: + resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color2k@2.0.3: + resolution: {integrity: sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==} + + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concurrently@9.2.0: + resolution: {integrity: sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==} + engines: {node: '>=18'} + hasBin: true + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + copy-to-clipboard@3.3.3: + resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-box-model@1.2.1: + resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + + css-in-js-utils@3.1.0: + resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} + + css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + 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-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + 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'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-uri-component@0.4.1: + resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} + engines: {node: '>=14.16'} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + discontinuous-range@1.0.0: + resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==} + + dockview-core@4.7.1: + resolution: {integrity: sha512-Tia3vYHtqACMZTiZv86yQOabwKj5KrBhQqlSr7qXV0qmmRSZ8dNbaU63LIHYFprST7JgHupIm9JVES+OhqMoTQ==} + + dockview@4.7.1: + resolution: {integrity: sha512-DgMzSKNjDvZzIQjFfAV6I6EDkqe40Sjz1Qgyf88KG4U1Kgp/bIIEDSLpz65BsW5ZD9Qi3y18TCISYTgsNvU9TA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + dpdm@3.14.0: + resolution: {integrity: sha512-YJzsFSyEtj88q5eTELg3UWU7TVZkG1dpbF4JDQ3t1b07xuzXmdoGeSz9TKOke1mUuOpWlk4q+pBh+aHzD6GBTg==} + hasBin: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + electron-to-chromium@1.5.186: + resolution: {integrity: sha512-lur7L4BFklgepaJxj4DqPk7vKbTEl0pajNlg2QjE5shefmlmBLm2HvQ7PMf1R/GvlevT/581cop33/quQcfX3A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + engine.io-client@6.6.3: + resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.1: + resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + es-toolkit@1.39.10: + resolution: {integrity: sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==} + + es-toolkit@1.39.7: + resolution: {integrity: sha512-ek/wWryKouBrZIjkwW2BFf91CWOIMvoy2AE5YYgUrfWsJQM2Su1LoLtrw8uusEpN9RfqLlV/0FVNjT0WMv8Bxw==} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.25.6: + resolution: {integrity: sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-i18next@6.1.2: + resolution: {integrity: sha512-hvTmws4kouNHkk314+9MHNj+RQmsqrkejWhTXGlRC0j8H+EXq2qDRLe6UqIjrFZo7/ogyd4btuqsnKCBi8wHbw==} + engines: {node: '>=18.10.0'} + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-path@2.0.3: + resolution: {integrity: sha512-VmVvqufPiviIViXK05Eg2OvCUh5RpXTmVdb5xQ1symoICf11d3LnFiEbdV/HGqM3BVJyQnNkyK/aduL+A1O3JA==} + engines: {node: '>= 12.22.0'} + peerDependencies: + eslint: '>=9.0.0' + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.20: + resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==} + peerDependencies: + eslint: '>=8.40' + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-plugin-simple-import-sort@12.1.1: + resolution: {integrity: sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==} + peerDependencies: + eslint: '>=5.0.0' + + eslint-plugin-storybook@9.0.17: + resolution: {integrity: sha512-IuTdlwCEwoDNobdygRCxNhlKXHmsDfPtPvHGcsY35x2Bx8KItrjfekO19gJrjc1VT2CMfcZMYF8OBKaxHELupw==} + engines: {node: '>=20.0.0'} + peerDependencies: + eslint: '>=8' + storybook: ^9.0.17 + + eslint-plugin-unused-imports@4.1.4: + resolution: {integrity: sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0 + eslint: ^9.0.0 || ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.31.0: + resolution: {integrity: sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.2.1: + resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-printf@1.6.10: + resolution: {integrity: sha512-GwTgG9O4FVIdShhbVF3JxOgSBY2+ePGsu2V/UONgoCPzF9VY6ZdBMKsHKCYQHZwNk3qNouUolRDsgVxcVA5G1w==} + engines: {node: '>=10.0'} + + fast-shallow-equal@1.0.0: + resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} + + fastest-stable-stringify@2.0.2: + resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-selector@2.1.2: + resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} + engines: {node: '>= 12'} + + filesize@10.1.6: + resolution: {integrity: sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==} + engines: {node: '>= 10.4.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + filter-obj@5.1.0: + resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} + engines: {node: '>=14.16'} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + focus-lock@1.3.6: + resolution: {integrity: sha512-Ik/6OCk9RQQ0T5Xw+hKNLWrjSMtv51dD4GRmJjbD5a58TIEpI5a5iXagKVl3Z5UuyslMCA8Xwnu76jQob62Yhg==} + engines: {node: '>=10'} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + formatly@0.2.4: + resolution: {integrity: sha512-lIN7GpcvX/l/i24r/L9bnJ0I8Qn01qijWpQpDDvTLL29nKqSaJJu4h20+7VJ6m2CAhQ2/En/GbxDiHCzq/0MyA==} + engines: {node: '>=18.3.0'} + hasBin: true + + fracturedjsonjs@4.1.0: + resolution: {integrity: sha512-qy6LPA8OOiiyRHt5/sNKDayD7h5r3uHmHxSOLbBsgtU/hkt5vOVWOR51MdfDbeCNfj7k/dKCRbXYm8FBAJcgWQ==} + + framer-motion@10.18.0: + resolution: {integrity: sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + framer-motion@11.18.2: + resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + framesync@6.1.2: + resolution: {integrity: sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==} + + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.3.0: + resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + hyphenate-style-name@1.1.0: + resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} + + i18next-http-backend@3.0.2: + resolution: {integrity: sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==} + + i18next@25.3.2: + resolution: {integrity: sha512-JSnbZDxRVbphc5jiptxr3o2zocy5dEqpVm9qCGdJwRNO+9saUJS0/u4LnM/13C23fUEWxAylPqKU/NpMV/IjqA==} + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + + idb-keyval@6.2.1: + resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + index-to-position@1.1.0: + resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==} + engines: {node: '>=18'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + inline-style-prefixer@7.0.1: + resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + js-cookie@2.2.1: + resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} + + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsondiffpatch@0.7.3: + resolution: {integrity: sha512-zd4dqFiXSYyant2WgSXAZ9+yYqilNVvragVNkNRn2IFZKgjyULNrKRznqN4Zon0MkLueCg+3QaPVCnDAVP20OQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + knip@5.61.3: + resolution: {integrity: sha512-8iSz8i8ufIjuUwUKzEwye7ROAW0RzCze7T770bUiz0PKL+SSwbs4RS32fjMztLwcOzSsNPlXdUAeqmkdzXxJ1Q==} + engines: {node: '>=18.18.0'} + hasBin: true + peerDependencies: + '@types/node': '>=18' + typescript: '>=5.0.4' + + konva@9.3.22: + resolution: {integrity: sha512-yQI5d1bmELlD/fowuyfOp9ff+oamg26WOCkyqUyc+nczD/lhRa3EvD2MZOoc4c1293TAubW9n34fSQLgSeEgSw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + linkify-react@4.3.1: + resolution: {integrity: sha512-w8ahBdCwF9C/doS4V3nE93QF1oyORmosvi8UEUbpHYws077eGzhkxUzJQcE2/SU5Q2K7SD80M4ybwwZGHErx5Q==} + peerDependencies: + linkifyjs: ^4.0.0 + react: '>= 15.0.0' + + linkifyjs@4.3.1: + resolution: {integrity: sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==} + + liqe@3.8.3: + resolution: {integrity: sha512-kjx7gTyYuhhw5b0KMP2DP8fxVPMr29L4B8pjdAN0t/saJejlIw5GpBldz5EeKaHtsrKlOnj6hjpsoXDfxnEtpQ==} + engines: {node: '>=12.0'} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.1.4: + resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.1.0: + resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + math-expression-evaluator@2.0.7: + resolution: {integrity: sha512-uwliJZ6BPHRq4eiqNWxZBDzKUiS5RIynFFcgchqhBOloVLVBpZpNG8jRYkedLcBvhph8TnRyWEuxPqiQcwIdog==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + moo@0.5.2: + resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} + + motion-dom@11.18.1: + resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==} + + motion-utils@11.18.1: + resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mtwist@1.0.2: + resolution: {integrity: sha512-eRsSga5jkLg7nNERPOV8vDNxgSwuEcj5upQfJcT0gXfJwXo3pMc7xOga0fu8rXHyrxzl7GFVWWDuaPQgpKDvgw==} + + nano-css@5.6.2: + resolution: {integrity: sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==} + peerDependencies: + react: '*' + react-dom: '*' + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.1.5: + resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} + engines: {node: ^18 || >=20} + hasBin: true + + nanostores@1.0.1: + resolution: {integrity: sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw==} + engines: {node: ^20.0.0 || >=22.0.0} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + nearley@2.20.1: + resolution: {integrity: sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==} + hasBin: true + + new-github-issue-url@1.1.0: + resolution: {integrity: sha512-R4r7f3Q/SzlI4Q/J/0KPRf+bwxYk7BiaYEy0zTVqpikA5F1CwCHgwVReKhpYRlG1besvLdtABQGQRhFy8CyT3g==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + + openapi-typescript@7.8.0: + resolution: {integrity: sha512-1EeVWmDzi16A+siQlo/SwSGIT7HwaFAVjvMA7/jG5HMLSnrUOzPL7uSTRZZa4v/LCRxHTApHKtNY6glApEoiUQ==} + hasBin: true + peerDependencies: + typescript: ^5.x + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + overlayscrollbars-react@0.5.6: + resolution: {integrity: sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==} + peerDependencies: + overlayscrollbars: ^2.0.0 + react: '>=16.8.0' + + overlayscrollbars@2.11.4: + resolution: {integrity: sha512-GKYQo3OZ1QWnppNjQVv5hfpn+glYUxc6+ufW+ivdXUyLWFNc01XoH2Z36KGM4I8e5pXYeA3ElNItcXiLvmUhnQ==} + + overlayscrollbars@2.12.0: + resolution: {integrity: sha512-mWJ5MOkcZ/ljHwfLw8+bN0V9ziGCoNoqULcp994j5DTGNQvnkWKWkA7rnO29Kyew5AoHxUnJ4Ndqfcl0HSQjXg==} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + oxc-resolver@11.4.0: + resolution: {integrity: sha512-J19ZMuUoGTsMc7TUacC5B5LQccZ6CluLmQ/RiC9mXKVvC8RCoiLjQOjEKrVolvxeU9q+TK1hrcJnmtZi/DqA6Q==} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + perfect-freehand@1.2.2: + resolution: {integrity: sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + query-string@9.2.2: + resolution: {integrity: sha512-pDSIZJ9sFuOp6VnD+5IkakSVf+rICAuuU88Hcsr6AKL0QtxSIfVuKiVP2oahFI7tk3CRSexwV+Ya6MOoTxzg9g==} + engines: {node: '>=18'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + raf-schd@4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + + raf-throttle@2.0.6: + resolution: {integrity: sha512-C7W6hy78A+vMmk5a/B6C5szjBHrUzWJkVyakjKCK59Uy2CcA7KhO1JUvvH32IXYFIcyJ3FMKP3ZzCc2/71I6Vg==} + + railroad-diagrams@1.0.0: + resolution: {integrity: sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==} + + randexp@0.4.6: + resolution: {integrity: sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==} + engines: {node: '>=0.12'} + + react-clientside-effect@1.2.8: + resolution: {integrity: sha512-ma2FePH0z3px2+WOu6h+YycZcEvFmmxIlAb62cF52bG86eMySciO/EQZeQMXd07kPCYB0a1dWDT5J+KE9mCDUw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + react-colorful@5.6.1: + resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + react-docgen-typescript@2.4.0: + resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==} + peerDependencies: + typescript: '>= 4.3.x' + + react-docgen@8.0.0: + resolution: {integrity: sha512-kmob/FOTwep7DUWf9KjuenKX0vyvChr3oTdvvPt09V60Iz75FJp+T/0ZeHMbAfJj2WaVWqAPP5Hmm3PYzSPPKg==} + engines: {node: ^20.9.0 || >=22} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-dropzone@14.3.8: + resolution: {integrity: sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + + react-error-boundary@5.0.0: + resolution: {integrity: sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ==} + peerDependencies: + react: '>=16.13.1' + + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + react-focus-lock@2.13.6: + resolution: {integrity: sha512-ehylFFWyYtBKXjAO9+3v8d0i+cnc1trGS0vlTGhzFW1vbFXVUTmR8s2tt/ZQG8x5hElg6rhENlLG1H3EZK0Llg==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-hook-form@7.60.0: + resolution: {integrity: sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-hotkeys-hook@4.5.0: + resolution: {integrity: sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==} + peerDependencies: + react: '>=16.8.1' + react-dom: '>=16.8.1' + + react-i18next@15.6.0: + resolution: {integrity: sha512-W135dB0rDfiFmbMipC17nOhGdttO5mzH8BivY+2ybsQBbXvxWIwl3cmeH3T9d+YPBSJu/ouyJKFJTtkK7rJofw==} + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + + react-i18next@15.7.3: + resolution: {integrity: sha512-AANws4tOE+QSq/IeMF/ncoHlMNZaVLxpa5uUGW1wjike68elVYr0018L9xYoqBr1OFO7G7boDPrbn0HpMCJxTw==} + peerDependencies: + i18next: '>= 25.4.1' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + + react-icons@5.5.0: + resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} + peerDependencies: + react: '*' + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.1: + resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-resizable-panels@3.0.3: + resolution: {integrity: sha512-7HA8THVBHTzhDK4ON0tvlGXyMAJN1zBeRpuyyremSikgYh2ku6ltD7tsGQOcXx4NKPrZtYCm/5CBr+dkruTGQw==} + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + react-router-dom@7.12.0: + resolution: {integrity: sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.12.0: + resolution: {integrity: sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-select@5.10.2: + resolution: {integrity: sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-select@5.8.3: + resolution: {integrity: sha512-lVswnIq8/iTj1db7XCG74M/3fbGB6ZaluCzvwPGT5ZOjCdL/k0CLWhEK0vCBLuU5bHTEf6Gj8jtSvi+3v+tO1w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-textarea-autosize@8.5.9: + resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==} + engines: {node: '>=10'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react-universal-interface@0.6.2: + resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==} + peerDependencies: + react: '*' + tslib: '*' + + react-use@17.6.0: + resolution: {integrity: sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==} + peerDependencies: + react: '*' + react-dom: '*' + + react-virtuoso@4.13.0: + resolution: {integrity: sha512-XHv2Fglpx80yFPdjZkV9d1baACKghg/ucpDFEXwaix7z0AfVQj+mF6lM+YQR6UC/TwzXG2rJKydRMb3+7iV3PA==} + peerDependencies: + react: '>=16 || >=17 || >= 18 || >= 19' + react-dom: '>=16 || >=17 || >= 18 || >=19' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + redux-remember@5.2.0: + resolution: {integrity: sha512-HqXx9V+DKzgBzpiIT5dyiXZgiiSB6zaMs4sIscwQ+Z0zVwUvJh20mqPEQWo4wbthuo5+5jGrS7Yfvv4HyOuAFw==} + peerDependencies: + redux: '>=5.0.0' + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux-undo@1.1.0: + resolution: {integrity: sha512-zzLFh2qeF0MTIlzDhDLm9NtkfBqCllQJ3OCuIl5RKlG/ayHw6GUdIFdMhzMS9NnrnWdBX5u//ExMOHpfudGGOg==} + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + requireindex@1.1.0: + resolution: {integrity: sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==} + engines: {node: '>=0.10.5'} + + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + + resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + ret@0.1.15: + resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} + engines: {node: '>=0.12'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + roarr@7.21.1: + resolution: {integrity: sha512-3niqt5bXFY1InKU8HKWqqYTYjtrBaxBMnXELXCXUYgtNYGUtZM5rB46HIC430AyacL95iEniGf7RgqsesykLmQ==} + engines: {node: '>=18.0'} + + rollup-plugin-visualizer@6.0.3: + resolution: {integrity: sha512-ZU41GwrkDcCpVoffviuM9Clwjy5fcUxlz0oMoTXTYsK+tcIFzbdacnrr2n8TXcHxbGKKXtOdjxM2HUS4HjkwIw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + rolldown: 1.x || ^1.0.0-beta + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rolldown: + optional: true + rollup: + optional: true + + rollup@2.79.2: + resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==} + engines: {node: '>=10.0.0'} + hasBin: true + + rollup@4.45.1: + resolution: {integrity: sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rtl-css-js@1.16.1: + resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + screenfull@5.2.0: + resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} + engines: {node: '>=0.10.0'} + + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + serialize-error@12.0.0: + resolution: {integrity: sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==} + engines: {node: '>=18'} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-harmonic-interval@1.0.1: + resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==} + engines: {node: '>=6.9'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + engines: {node: '>=18'} + + smol-toml@1.4.1: + resolution: {integrity: sha512-CxdwHXyYTONGHThDbq5XdwbFsuY4wlClRGejfE2NtwUtiHYsP1QtNsHb/hnj31jKYSchztJsaA8pSQoVzkfCFg==} + engines: {node: '>= 18'} + + socket.io-client@4.8.1: + resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.5.6: + resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==} + engines: {node: '>=0.10.0'} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + split-on-first@3.0.0: + resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==} + engines: {node: '>=12'} + + stable-hash@0.0.6: + resolution: {integrity: sha512-0afH4mobqTybYZsXImQRLOjHV4gvOW+92HdUIax9t7a8d9v54KWykEuMVIcXhD9BCi+w3kS4x7O6fmZQ3JlG/g==} + + stack-generator@2.0.10: + resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + stacktrace-gps@3.1.2: + resolution: {integrity: sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==} + + stacktrace-js@2.0.2: + resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + storybook@9.0.17: + resolution: {integrity: sha512-O+9jgJ+Trlq9VGD1uY4OBLKQWHHDKM/A/pA8vMW6PVehhGHNvpzcIC1bngr6mL5gGHZP2nBv+9XG8pTMcggMmg==} + hasBin: true + peerDependencies: + prettier: ^2 || ^3 + peerDependenciesMeta: + prettier: + optional: true + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-indent@4.0.0: + resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-json-comments@5.0.2: + resolution: {integrity: sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==} + engines: {node: '>=14.16'} + + strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + + supports-color@10.0.0: + resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==} + engines: {node: '>=18'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} + + throttle-debounce@3.0.1: + resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} + engines: {node: '>=10'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.3: + resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toggle-selection@1.0.6: + resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + ts-easing@0.2.0: + resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==} + + ts-error@1.0.6: + resolution: {integrity: sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==} + + tsafe@1.8.5: + resolution: {integrity: sha512-LFWTWQrW6rwSY+IBNFl2ridGfUzVsPwrZ26T4KUJww/py8rzaQ/SY+MIz6YROozpUCaRcuISqagmlwub9YT9kw==} + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.4.0: + resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unplugin@1.16.1: + resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} + engines: {node: '>=14.0.0'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-composed-ref@1.4.0: + resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-debounce@10.0.5: + resolution: {integrity: sha512-Q76E3lnIV+4YT9AHcrHEHYmAd9LKwUAbPXDm7FlqVGDHiSOhX3RDjT8dm0AxbJup6WgOb1YEcKyCr11kBJR5KQ==} + engines: {node: '>= 16.0.0'} + peerDependencies: + react: '*' + + use-device-pixel-ratio@1.1.2: + resolution: {integrity: sha512-nFxV0HwLdRUt20kvIgqHYZe6PK/v4mU1X8/eLsT1ti5ck0l2ob0HDRziaJPx+YWzBo6dMm4cTac3mcyk68Gh+A==} + peerDependencies: + react: '>=16.8.0' + + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-latest@1.3.0: + resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + 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 + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-plugin-eslint@1.8.1: + resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==} + peerDependencies: + eslint: '>=7' + vite: '>=2' + + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@7.0.5: + resolution: {integrity: sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==} + 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 + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + + zod-validation-error@3.5.3: + resolution: {integrity: sha512-OT5Y8lbUadqVZCsnyFaTQ4/O2mys4tj7PqhdbBCp7McPwvIEKfPtdA6QfPeFQK2/Rz5LgwmAXRJTugBNBi0btw==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.0.10: + resolution: {integrity: sha512-3vB+UU3/VmLL2lvwcY/4RV2i9z/YU0DTV/tDuYjrwmx5WeJ7hwy+rGEEx8glHp6Yxw7ibRbKSaIFBgReRPe5KA==} + + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + +snapshots: + + '@adobe/css-tools@4.4.3': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + + '@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.1': + dependencies: + '@atlaskit/pragmatic-drag-and-drop': 1.7.4 + '@babel/runtime': 7.27.6 + + '@atlaskit/pragmatic-drag-and-drop-hitbox@1.1.0': + dependencies: + '@atlaskit/pragmatic-drag-and-drop': 1.7.4 + '@babel/runtime': 7.27.6 + + '@atlaskit/pragmatic-drag-and-drop@1.7.4': + dependencies: + '@babel/runtime': 7.27.6 + bind-event-listener: 3.0.0 + raf-schd: 4.0.3 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.0': {} + + '@babel/core@7.28.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helpers': 7.27.6 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + convert-source-map: 2.0.0 + debug: 4.4.1(supports-color@10.0.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.0': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.27.6': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.1 + + '@babel/parser@7.28.0': + dependencies: + '@babel/types': 7.28.0 + + '@babel/runtime@7.27.6': {} + + '@babel/runtime@7.28.3': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + + '@babel/traverse@7.28.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/types': 7.28.0 + debug: 4.4.1(supports-color@10.0.0) + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@babel/types@7.28.1': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@bcoe/v8-coverage@1.0.2': {} + + '@chakra-ui/anatomy@2.2.2': {} + + '@chakra-ui/anatomy@2.3.4': {} + + '@chakra-ui/anatomy@2.3.6': {} + + '@chakra-ui/breakpoint-utils@2.0.8': + dependencies: + '@chakra-ui/shared-utils': 2.0.5 + + '@chakra-ui/color-mode@2.2.0(react@18.3.1)': + dependencies: + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) + react: 18.3.1 + + '@chakra-ui/hooks@2.4.5(react@18.3.1)': + dependencies: + '@chakra-ui/utils': 2.2.5(react@18.3.1) + '@zag-js/element-size': 0.31.1 + copy-to-clipboard: 3.3.3 + framesync: 6.1.2 + react: 18.3.1 + + '@chakra-ui/icon@3.2.0(@chakra-ui/system@2.6.2(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.6.2(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(react@18.3.1) + react: 18.3.1 + + '@chakra-ui/icons@2.2.4(@chakra-ui/react@2.10.9(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(framer-motion@10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@chakra-ui/react': 2.10.9(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(framer-motion@11.18.2(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + + '@chakra-ui/layout@2.3.1(@chakra-ui/system@2.6.2(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@chakra-ui/breakpoint-utils': 2.0.8 + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@chakra-ui/object-utils': 2.1.0 + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.6.2(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(react@18.3.1) + react: 18.3.1 + + '@chakra-ui/object-utils@2.1.0': {} + + '@chakra-ui/portal@2.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@chakra-ui/react-children-utils@2.0.6(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@chakra-ui/react-context@2.1.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@chakra-ui/react-use-safe-layout-effect@2.1.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@chakra-ui/react-utils@2.0.12(react@18.3.1)': + dependencies: + '@chakra-ui/utils': 2.0.15 + react: 18.3.1 + + '@chakra-ui/react@2.10.9(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(framer-motion@11.18.2(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@chakra-ui/hooks': 2.4.5(react@18.3.1) + '@chakra-ui/styled-system': 2.12.4(react@18.3.1) + '@chakra-ui/theme': 3.4.9(@chakra-ui/styled-system@2.12.4(react@18.3.1))(react@18.3.1) + '@chakra-ui/utils': 2.2.5(react@18.3.1) + '@emotion/react': 11.14.0(@types/react@18.3.23)(react@18.3.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) + '@popperjs/core': 2.11.8 + '@zag-js/focus-visible': 0.31.1 + aria-hidden: 1.2.6 + framer-motion: 11.18.2(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-fast-compare: 3.2.2 + react-focus-lock: 2.13.6(@types/react@18.3.23)(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + + '@chakra-ui/shared-utils@2.0.5': {} + + '@chakra-ui/styled-system@2.12.0(react@18.3.1)': + dependencies: + '@chakra-ui/utils': 2.2.2(react@18.3.1) + csstype: 3.1.3 + transitivePeerDependencies: + - react + + '@chakra-ui/styled-system@2.12.4(react@18.3.1)': + dependencies: + '@chakra-ui/utils': 2.2.5(react@18.3.1) + csstype: 3.1.3 + transitivePeerDependencies: + - react + + '@chakra-ui/styled-system@2.9.2': + dependencies: + '@chakra-ui/shared-utils': 2.0.5 + csstype: 3.1.3 + lodash.mergewith: 4.6.2 + + '@chakra-ui/system@2.6.2(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(react@18.3.1)': + dependencies: + '@chakra-ui/color-mode': 2.2.0(react@18.3.1) + '@chakra-ui/object-utils': 2.1.0 + '@chakra-ui/react-utils': 2.0.12(react@18.3.1) + '@chakra-ui/styled-system': 2.9.2 + '@chakra-ui/theme-utils': 2.0.21 + '@chakra-ui/utils': 2.0.15 + '@emotion/react': 11.14.0(@types/react@18.3.23)(react@18.3.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-fast-compare: 3.2.2 + + '@chakra-ui/theme-tools@2.1.2(@chakra-ui/styled-system@2.9.2)': + dependencies: + '@chakra-ui/anatomy': 2.2.2 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/styled-system': 2.9.2 + color2k: 2.0.3 + + '@chakra-ui/theme-tools@2.2.6(@chakra-ui/styled-system@2.12.0(react@18.3.1))(react@18.3.1)': + dependencies: + '@chakra-ui/anatomy': 2.3.4 + '@chakra-ui/styled-system': 2.12.0(react@18.3.1) + '@chakra-ui/utils': 2.2.2(react@18.3.1) + color2k: 2.0.3 + transitivePeerDependencies: + - react + + '@chakra-ui/theme-tools@2.2.9(@chakra-ui/styled-system@2.12.4(react@18.3.1))(react@18.3.1)': + dependencies: + '@chakra-ui/anatomy': 2.3.6 + '@chakra-ui/styled-system': 2.12.4(react@18.3.1) + '@chakra-ui/utils': 2.2.5(react@18.3.1) + color2k: 2.0.3 + transitivePeerDependencies: + - react + + '@chakra-ui/theme-utils@2.0.21': + dependencies: + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/styled-system': 2.9.2 + '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) + lodash.mergewith: 4.6.2 + + '@chakra-ui/theme@3.3.1(@chakra-ui/styled-system@2.9.2)': + dependencies: + '@chakra-ui/anatomy': 2.2.2 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/styled-system': 2.9.2 + '@chakra-ui/theme-tools': 2.1.2(@chakra-ui/styled-system@2.9.2) + + '@chakra-ui/theme@3.4.9(@chakra-ui/styled-system@2.12.4(react@18.3.1))(react@18.3.1)': + dependencies: + '@chakra-ui/anatomy': 2.3.6 + '@chakra-ui/styled-system': 2.12.4(react@18.3.1) + '@chakra-ui/theme-tools': 2.2.9(@chakra-ui/styled-system@2.12.4(react@18.3.1))(react@18.3.1) + '@chakra-ui/utils': 2.2.5(react@18.3.1) + transitivePeerDependencies: + - react + + '@chakra-ui/utils@2.0.15': + dependencies: + '@types/lodash.mergewith': 4.6.7 + css-box-model: 1.2.1 + framesync: 6.1.2 + lodash.mergewith: 4.6.2 + + '@chakra-ui/utils@2.2.2(react@18.3.1)': + dependencies: + '@types/lodash.mergewith': 4.6.9 + lodash.mergewith: 4.6.2 + react: 18.3.1 + + '@chakra-ui/utils@2.2.5(react@18.3.1)': + dependencies: + '@types/lodash.mergewith': 4.6.9 + lodash.mergewith: 4.6.2 + react: 18.3.1 + + '@dagrejs/dagre@1.1.5': + dependencies: + '@dagrejs/graphlib': 2.2.4 + + '@dagrejs/graphlib@2.2.4': {} + + '@dmsnell/diff-match-patch@1.1.0': {} + + '@emnapi/core@1.4.3': + dependencies: + '@emnapi/wasi-threads': 1.0.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/runtime': 7.27.6 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/is-prop-valid@0.8.8': + dependencies: + '@emotion/memoize': 0.7.4 + optional: true + + '@emotion/is-prop-valid@1.3.1': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.7.4': + optional: true + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.27.6 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.1.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.3 + '@emotion/babel-plugin': 11.13.5 + '@emotion/is-prop-valid': 1.3.1 + '@emotion/react': 11.14.0(@types/react@18.3.23)(react@18.3.1) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@emotion/utils': 1.4.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + transitivePeerDependencies: + - supports-color + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@esbuild/aix-ppc64@0.25.6': + optional: true + + '@esbuild/android-arm64@0.25.6': + optional: true + + '@esbuild/android-arm@0.25.6': + optional: true + + '@esbuild/android-x64@0.25.6': + optional: true + + '@esbuild/darwin-arm64@0.25.6': + optional: true + + '@esbuild/darwin-x64@0.25.6': + optional: true + + '@esbuild/freebsd-arm64@0.25.6': + optional: true + + '@esbuild/freebsd-x64@0.25.6': + optional: true + + '@esbuild/linux-arm64@0.25.6': + optional: true + + '@esbuild/linux-arm@0.25.6': + optional: true + + '@esbuild/linux-ia32@0.25.6': + optional: true + + '@esbuild/linux-loong64@0.25.6': + optional: true + + '@esbuild/linux-mips64el@0.25.6': + optional: true + + '@esbuild/linux-ppc64@0.25.6': + optional: true + + '@esbuild/linux-riscv64@0.25.6': + optional: true + + '@esbuild/linux-s390x@0.25.6': + optional: true + + '@esbuild/linux-x64@0.25.6': + optional: true + + '@esbuild/netbsd-arm64@0.25.6': + optional: true + + '@esbuild/netbsd-x64@0.25.6': + optional: true + + '@esbuild/openbsd-arm64@0.25.6': + optional: true + + '@esbuild/openbsd-x64@0.25.6': + optional: true + + '@esbuild/openharmony-arm64@0.25.6': + optional: true + + '@esbuild/sunos-x64@0.25.6': + optional: true + + '@esbuild/win32-arm64@0.25.6': + optional: true + + '@esbuild/win32-ia32@0.25.6': + optional: true + + '@esbuild/win32-x64@0.25.6': + optional: true + + '@eslint-community/eslint-utils@4.7.0(eslint@9.31.0(jiti@2.4.2))': + dependencies: + eslint: 9.31.0(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1(supports-color@10.0.0) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.0': {} + + '@eslint/core@0.15.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1(supports-color@10.0.0) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.31.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.3': + dependencies: + '@eslint/core': 0.15.1 + levn: 0.4.1 + + '@floating-ui/core@1.7.2': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.2': + dependencies: + '@floating-ui/core': 1.7.2 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@fontsource-variable/inter@5.2.6': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@invoke-ai/ui-library@https://codeload.github.com/invoke-ai/ui-library/tar.gz/c8a1d9a6867cd05ac831b074876c8dca3b0b670d(@chakra-ui/system@2.6.2(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(react@18.3.1))(@fontsource-variable/inter@5.2.6)(@types/react@18.3.23)(i18next@25.3.2(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)': + dependencies: + '@chakra-ui/anatomy': 2.3.4 + '@chakra-ui/icons': 2.2.4(@chakra-ui/react@2.10.9(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(framer-motion@10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@chakra-ui/react': 2.10.9(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(framer-motion@11.18.2(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@chakra-ui/styled-system': 2.12.0(react@18.3.1) + '@chakra-ui/theme-tools': 2.2.6(@chakra-ui/styled-system@2.12.0(react@18.3.1))(react@18.3.1) + '@emotion/react': 11.14.0(@types/react@18.3.23)(react@18.3.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) + '@fontsource-variable/inter': 5.2.6 + '@nanostores/react': 1.0.0(nanostores@1.0.1)(react@18.3.1) + chakra-react-select: 4.10.1(@chakra-ui/react@2.10.9(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(framer-motion@10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + es-toolkit: 1.39.10 + framer-motion: 10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + math-expression-evaluator: 2.0.7 + nanostores: 1.0.1 + overlayscrollbars: 2.12.0 + overlayscrollbars-react: 0.5.6(overlayscrollbars@2.12.0)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-i18next: 15.7.3(i18next@25.3.2(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + react-icons: 5.5.0(react@18.3.1) + react-select: 5.10.2(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - '@chakra-ui/system' + - '@types/react' + - i18next + - react-native + - supports-color + - typescript + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.8.3)(vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2))': + dependencies: + glob: 10.4.5 + magic-string: 0.30.17 + react-docgen-typescript: 2.4.0(typescript@5.8.3) + vite: 7.0.5(@types/node@22.16.0)(jiti@2.4.2) + optionalDependencies: + typescript: 5.8.3 + + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.4': {} + + '@jridgewell/trace-mapping@0.3.29': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 + + '@mdx-js/react@3.1.0(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@types/mdx': 2.0.13 + '@types/react': 18.3.23 + react: 18.3.1 + + '@nanostores/react@1.0.0(nanostores@1.0.1)(react@18.3.1)': + dependencies: + nanostores: 1.0.1 + react: 18.3.1 + + '@napi-rs/wasm-runtime@0.2.11': + dependencies: + '@emnapi/core': 1.4.3 + '@emnapi/runtime': 1.4.3 + '@tybys/wasm-util': 0.9.0 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@observ33r/object-equals@1.1.5': {} + + '@oxc-resolver/binding-darwin-arm64@11.4.0': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.4.0': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.4.0': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.4.0': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.4.0': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.4.0': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.4.0': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.4.0': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.4.0': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.4.0': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.4.0': + dependencies: + '@napi-rs/wasm-runtime': 0.2.11 + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.4.0': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.4.0': + optional: true + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@polka/url@1.0.0-next.29': {} + + '@popperjs/core@2.11.8': {} + + '@radix-ui/primitive@1.1.2': {} + + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-context@1.1.2(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-dialog@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-id@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-slot@1.2.3(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.22.2': {} + + '@redocly/openapi-core@1.34.3(supports-color@10.0.0)': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.22.2 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@10.0.0) + js-levenshtein: 1.1.6 + js-yaml: 4.1.0 + minimatch: 5.1.6 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + + '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@18.3.23)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': + dependencies: + '@standard-schema/spec': 1.0.0 + '@standard-schema/utils': 0.3.0 + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 18.3.1 + react-redux: 9.2.0(@types/react@18.3.23)(react@18.3.1)(redux@5.0.1) + + '@roarr/browser-log-writer@1.3.0': + dependencies: + boolean: 3.2.0 + globalthis: 1.0.4 + liqe: 3.8.3 + + '@rolldown/pluginutils@1.0.0-beta.11': {} + + '@rollup/pluginutils@4.2.1': + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + + '@rollup/pluginutils@5.2.0(rollup@4.45.1)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.45.1 + + '@rollup/rollup-android-arm-eabi@4.45.1': + optional: true + + '@rollup/rollup-android-arm64@4.45.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.45.1': + optional: true + + '@rollup/rollup-darwin-x64@4.45.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.45.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.45.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.45.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.45.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.45.1': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.45.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.45.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.45.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.45.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.45.1': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@socket.io/component-emitter@3.1.2': {} + + '@standard-schema/spec@1.0.0': {} + + '@standard-schema/utils@0.3.0': {} + + '@storybook/addon-docs@9.0.17(@types/react@18.3.23)(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2))': + dependencies: + '@mdx-js/react': 3.1.0(@types/react@18.3.23)(react@18.3.1) + '@storybook/csf-plugin': 9.0.17(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2)) + '@storybook/icons': 1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/react-dom-shim': 9.0.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2)) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2) + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/react' + + '@storybook/addon-links@9.0.17(react@18.3.1)(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2))': + dependencies: + '@storybook/global': 5.0.0 + storybook: 9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2) + optionalDependencies: + react: 18.3.1 + + '@storybook/builder-vite@9.0.17(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2))(vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2))': + dependencies: + '@storybook/csf-plugin': 9.0.17(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2)) + storybook: 9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2) + ts-dedent: 2.2.0 + vite: 7.0.5(@types/node@22.16.0)(jiti@2.4.2) + + '@storybook/csf-plugin@9.0.17(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2))': + dependencies: + storybook: 9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2) + unplugin: 1.16.1 + + '@storybook/global@5.0.0': {} + + '@storybook/icons@1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@storybook/react-dom-shim@9.0.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2))': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2) + + '@storybook/react-vite@9.0.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3)(vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2))': + dependencies: + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.8.3)(vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2)) + '@rollup/pluginutils': 5.2.0(rollup@4.45.1) + '@storybook/builder-vite': 9.0.17(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2))(vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2)) + '@storybook/react': 9.0.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3) + find-up: 7.0.0 + magic-string: 0.30.17 + react: 18.3.1 + react-docgen: 8.0.0 + react-dom: 18.3.1(react@18.3.1) + resolve: 1.22.10 + storybook: 9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2) + tsconfig-paths: 4.2.0 + vite: 7.0.5(@types/node@22.16.0)(jiti@2.4.2) + transitivePeerDependencies: + - rollup + - supports-color + - typescript + + '@storybook/react@9.0.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/react-dom-shim': 9.0.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2)) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2) + optionalDependencies: + typescript: 5.8.3 + + '@swc/core-darwin-arm64@1.12.9': + optional: true + + '@swc/core-darwin-x64@1.12.9': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.12.9': + optional: true + + '@swc/core-linux-arm64-gnu@1.12.9': + optional: true + + '@swc/core-linux-arm64-musl@1.12.9': + optional: true + + '@swc/core-linux-x64-gnu@1.12.9': + optional: true + + '@swc/core-linux-x64-musl@1.12.9': + optional: true + + '@swc/core-win32-arm64-msvc@1.12.9': + optional: true + + '@swc/core-win32-ia32-msvc@1.12.9': + optional: true + + '@swc/core-win32-x64-msvc@1.12.9': + optional: true + + '@swc/core@1.12.9': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.23 + optionalDependencies: + '@swc/core-darwin-arm64': 1.12.9 + '@swc/core-darwin-x64': 1.12.9 + '@swc/core-linux-arm-gnueabihf': 1.12.9 + '@swc/core-linux-arm64-gnu': 1.12.9 + '@swc/core-linux-arm64-musl': 1.12.9 + '@swc/core-linux-x64-gnu': 1.12.9 + '@swc/core-linux-x64-musl': 1.12.9 + '@swc/core-win32-arm64-msvc': 1.12.9 + '@swc/core-win32-ia32-msvc': 1.12.9 + '@swc/core-win32-x64-msvc': 1.12.9 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.23': + dependencies: + '@swc/counter': 0.1.3 + + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.28.3 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.6.3': + dependencies: + '@adobe/css-tools': 4.4.3 + aria-query: 5.3.2 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.21 + redent: 3.0.0 + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': + dependencies: + '@testing-library/dom': 10.4.0 + + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.7 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.1 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + + '@types/babel__traverse@7.20.7': + dependencies: + '@babel/types': 7.28.1 + + '@types/chai@5.2.2': + dependencies: + '@types/deep-eql': 4.0.2 + + '@types/d3-color@3.1.3': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-selection@3.0.11': {} + + '@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/deep-eql@4.0.2': {} + + '@types/doctrine@0.0.9': {} + + '@types/eslint@8.56.12': + dependencies: + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + + '@types/estree@1.0.8': {} + + '@types/js-cookie@2.2.7': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/lodash.mergewith@4.6.7': + dependencies: + '@types/lodash': 4.17.20 + + '@types/lodash.mergewith@4.6.9': + dependencies: + '@types/lodash': 4.17.20 + + '@types/lodash@4.17.20': {} + + '@types/mdx@2.0.13': {} + + '@types/node@22.16.0': + dependencies: + undici-types: 6.21.0 + + '@types/parse-json@4.0.2': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.23)': + dependencies: + '@types/react': 18.3.23 + + '@types/react-transition-group@4.4.12(@types/react@18.3.23)': + dependencies: + '@types/react': 18.3.23 + + '@types/react@18.3.23': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.1.3 + + '@types/resolve@1.20.6': {} + + '@types/use-sync-external-store@0.0.6': {} + + '@types/uuid@10.0.0': {} + + '@typescript-eslint/eslint-plugin@8.37.0(@typescript-eslint/parser@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.37.0 + '@typescript-eslint/type-utils': 8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.37.0 + eslint: 9.31.0(jiti@2.4.2) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.37.0 + '@typescript-eslint/types': 8.37.0 + '@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.37.0 + debug: 4.4.1(supports-color@10.0.0) + eslint: 9.31.0(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.37.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.37.0(typescript@5.8.3) + '@typescript-eslint/types': 8.37.0 + debug: 4.4.1(supports-color@10.0.0) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.37.0': + dependencies: + '@typescript-eslint/types': 8.37.0 + '@typescript-eslint/visitor-keys': 8.37.0 + + '@typescript-eslint/tsconfig-utils@8.37.0(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + + '@typescript-eslint/type-utils@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 8.37.0 + '@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + debug: 4.4.1(supports-color@10.0.0) + eslint: 9.31.0(jiti@2.4.2) + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.37.0': {} + + '@typescript-eslint/typescript-estree@8.37.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/project-service': 8.37.0(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.37.0(typescript@5.8.3) + '@typescript-eslint/types': 8.37.0 + '@typescript-eslint/visitor-keys': 8.37.0 + debug: 4.4.1(supports-color@10.0.0) + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.37.0 + '@typescript-eslint/types': 8.37.0 + '@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.3) + eslint: 9.31.0(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.37.0': + dependencies: + '@typescript-eslint/types': 8.37.0 + eslint-visitor-keys: 4.2.1 + + '@vitejs/plugin-react-swc@3.10.2(vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.11 + '@swc/core': 1.12.9 + vite: 7.0.5(@types/node@22.16.0)(jiti@2.4.2) + transitivePeerDependencies: + - '@swc/helpers' + + '@vitest/coverage-v8@3.2.4(vitest@3.2.4)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + ast-v8-to-istanbul: 0.3.3 + debug: 4.4.1(supports-color@10.0.0) + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + magic-string: 0.30.17 + magicast: 0.3.5 + std-env: 3.9.0 + test-exclude: 7.0.1 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jiti@2.4.2) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.2 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.1 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 7.0.5(@types/node@22.16.0)(jiti@2.4.2) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.0.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.17 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.3 + + '@vitest/ui@3.2.4(vitest@3.2.4)': + dependencies: + '@vitest/utils': 3.2.4 + fflate: 0.8.2 + flatted: 3.3.3 + pathe: 2.0.3 + sirv: 3.0.1 + tinyglobby: 0.2.14 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jiti@2.4.2) + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.1.4 + tinyrainbow: 2.0.0 + + '@xobotyi/scrollbar-width@1.9.5': {} + + '@xyflow/react@12.8.2(@types/react@18.3.23)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@xyflow/system': 0.0.66 + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.23)(immer@10.1.1)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + + '@xyflow/system@0.0.66': + dependencies: + '@types/d3-drag': 3.0.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + + '@zag-js/dom-query@0.31.1': {} + + '@zag-js/element-size@0.31.1': {} + + '@zag-js/focus-visible@0.31.1': + dependencies: + '@zag-js/dom-query': 0.31.1 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ag-psd@28.2.2: + dependencies: + base64-js: 1.5.1 + pako: 2.1.0 + + agent-base@7.1.3: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.1: {} + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + assertion-error@2.0.1: {} + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + ast-v8-to-istanbul@0.3.3: + dependencies: + '@jridgewell/trace-mapping': 0.3.29 + estree-walker: 3.0.3 + js-tokens: 9.0.1 + + async-function@1.0.0: {} + + async-mutex@0.5.0: + dependencies: + tslib: 2.8.1 + + attr-accept@2.2.5: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.27.6 + cosmiconfig: 7.1.0 + resolve: 1.22.10 + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + better-opn@3.0.2: + dependencies: + open: 8.4.2 + + bind-event-listener@3.0.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + boolean@3.2.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.25.1: + dependencies: + caniuse-lite: 1.0.30001727 + electron-to-chromium: 1.5.186 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.1) + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001727: {} + + chai@5.2.0: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.4 + pathval: 2.0.1 + + chai@5.2.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.4 + pathval: 2.0.1 + + chakra-react-select@4.10.1(@chakra-ui/react@2.10.9(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(framer-motion@10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@chakra-ui/react': 2.10.9(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(framer-motion@11.18.2(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@emotion/react': 11.14.0(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-select: 5.8.3(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - supports-color + + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + change-case@5.4.4: {} + + check-error@2.1.1: {} + + classcat@5.0.5: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color2k@2.0.3: {} + + colorette@1.4.0: {} + + commander@2.20.3: {} + + compare-versions@6.1.1: {} + + concat-map@0.0.1: {} + + concurrently@9.2.0: + dependencies: + chalk: 4.1.2 + lodash: 4.17.21 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + copy-to-clipboard@3.3.3: + dependencies: + toggle-selection: 1.0.6 + + core-util-is@1.0.3: {} + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + + cross-fetch@4.0.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-box-model@1.2.1: + dependencies: + tiny-invariant: 1.3.3 + + css-in-js-utils@3.1.0: + dependencies: + hyphenate-style-name: 1.1.0 + + css-tree@1.1.3: + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + + css.escape@1.5.1: {} + + csstype@3.1.3: {} + + d3-color@3.1.0: {} + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-ease@3.0.1: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-selection@3.0.0: {} + + 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) + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + debug@4.4.1(supports-color@10.0.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 10.0.0 + + decode-uri-component@0.4.1: {} + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-lazy-prop@2.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + dequal@2.0.3: {} + + detect-node-es@1.1.0: {} + + discontinuous-range@1.0.0: {} + + dockview-core@4.7.1: {} + + dockview@4.7.1(react@18.3.1): + dependencies: + dockview-core: 4.7.1 + react: 18.3.1 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.27.6 + csstype: 3.1.3 + + dpdm@3.14.0: + dependencies: + chalk: 4.1.2 + fs-extra: 11.3.0 + glob: 10.4.5 + ora: 5.4.1 + tslib: 2.8.1 + typescript: 5.8.3 + yargs: 17.7.2 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + electron-to-chromium@1.5.186: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + engine.io-client@6.6.3: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1 + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + + es-abstract@1.24.0: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + es-toolkit@1.39.10: {} + + es-toolkit@1.39.7: {} + + esbuild-register@3.6.0(esbuild@0.25.6): + dependencies: + debug: 4.4.1(supports-color@10.0.0) + esbuild: 0.25.6 + transitivePeerDependencies: + - supports-color + + esbuild@0.25.6: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.6 + '@esbuild/android-arm': 0.25.6 + '@esbuild/android-arm64': 0.25.6 + '@esbuild/android-x64': 0.25.6 + '@esbuild/darwin-arm64': 0.25.6 + '@esbuild/darwin-x64': 0.25.6 + '@esbuild/freebsd-arm64': 0.25.6 + '@esbuild/freebsd-x64': 0.25.6 + '@esbuild/linux-arm': 0.25.6 + '@esbuild/linux-arm64': 0.25.6 + '@esbuild/linux-ia32': 0.25.6 + '@esbuild/linux-loong64': 0.25.6 + '@esbuild/linux-mips64el': 0.25.6 + '@esbuild/linux-ppc64': 0.25.6 + '@esbuild/linux-riscv64': 0.25.6 + '@esbuild/linux-s390x': 0.25.6 + '@esbuild/linux-x64': 0.25.6 + '@esbuild/netbsd-arm64': 0.25.6 + '@esbuild/netbsd-x64': 0.25.6 + '@esbuild/openbsd-arm64': 0.25.6 + '@esbuild/openbsd-x64': 0.25.6 + '@esbuild/openharmony-arm64': 0.25.6 + '@esbuild/sunos-x64': 0.25.6 + '@esbuild/win32-arm64': 0.25.6 + '@esbuild/win32-ia32': 0.25.6 + '@esbuild/win32-x64': 0.25.6 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.31.0(jiti@2.4.2)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.31.0(jiti@2.4.2) + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + + eslint-plugin-i18next@6.1.2: + dependencies: + lodash: 4.17.21 + requireindex: 1.1.0 + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.31.0(jiti@2.4.2) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.31.0(jiti@2.4.2)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-path@2.0.3(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3): + dependencies: + '@typescript-eslint/types': 8.37.0 + '@typescript-eslint/utils': 8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.31.0(jiti@2.4.2) + load-tsconfig: 0.2.5 + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-react-hooks@5.2.0(eslint@9.31.0(jiti@2.4.2)): + dependencies: + eslint: 9.31.0(jiti@2.4.2) + + eslint-plugin-react-refresh@0.4.20(eslint@9.31.0(jiti@2.4.2)): + dependencies: + eslint: 9.31.0(jiti@2.4.2) + + eslint-plugin-react@7.37.5(eslint@9.31.0(jiti@2.4.2)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.1 + eslint: 9.31.0(jiti@2.4.2) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-plugin-simple-import-sort@12.1.1(eslint@9.31.0(jiti@2.4.2)): + dependencies: + eslint: 9.31.0(jiti@2.4.2) + + eslint-plugin-storybook@9.0.17(eslint@9.31.0(jiti@2.4.2))(storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3): + dependencies: + '@typescript-eslint/utils': 8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.31.0(jiti@2.4.2) + storybook: 9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2) + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.37.0(@typescript-eslint/parser@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2)): + dependencies: + eslint: 9.31.0(jiti@2.4.2) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.37.0(@typescript-eslint/parser@8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.31.0(jiti@2.4.2): + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.4.2)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.0 + '@eslint/core': 0.15.1 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.31.0 + '@eslint/plugin-kit': 0.3.3 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1(supports-color@10.0.0) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.4.2 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + expect-type@1.2.1: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-printf@1.6.10: {} + + fast-shallow-equal@1.0.0: {} + + fastest-stable-stringify@2.0.2: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + + fdir@6.4.6(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fdir@6.4.6(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fflate@0.8.2: {} + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-selector@2.1.2: + dependencies: + tslib: 2.8.1 + + filesize@10.1.6: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + filter-obj@5.1.0: {} + + find-root@1.1.0: {} + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-up@7.0.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + focus-lock@1.3.6: + dependencies: + tslib: 2.8.1 + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + formatly@0.2.4: + dependencies: + fd-package-json: 2.0.0 + + fracturedjsonjs@4.1.0: {} + + framer-motion@10.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + tslib: 2.8.1 + optionalDependencies: + '@emotion/is-prop-valid': 0.8.8 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + framer-motion@11.18.2(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + motion-dom: 11.18.1 + motion-utils: 11.18.1 + tslib: 2.8.1 + optionalDependencies: + '@emotion/is-prop-valid': 1.3.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + framesync@6.1.2: + dependencies: + tslib: 2.4.0 + + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + globals@14.0.0: {} + + globals@16.3.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globrex@0.1.2: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + html-escaper@2.0.2: {} + + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + + https-proxy-agent@7.0.6(supports-color@10.0.0): + dependencies: + agent-base: 7.1.3 + debug: 4.4.1(supports-color@10.0.0) + transitivePeerDependencies: + - supports-color + + hyphenate-style-name@1.1.0: {} + + i18next-http-backend@3.0.2: + dependencies: + cross-fetch: 4.0.0 + transitivePeerDependencies: + - encoding + + i18next@25.3.2(typescript@5.8.3): + dependencies: + '@babel/runtime': 7.27.6 + optionalDependencies: + typescript: 5.8.3 + + idb-keyval@6.2.1: {} + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + immediate@3.0.6: {} + + immer@10.1.1: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + index-to-position@1.1.0: {} + + inherits@2.0.4: {} + + inline-style-prefixer@7.0.1: + dependencies: + css-in-js-utils: 3.1.0 + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-unicode-supported@0.1.0: {} + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.29 + debug: 4.4.1(supports-color@10.0.0) + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@2.4.2: {} + + js-cookie@2.2.1: {} + + js-levenshtein@1.1.6: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsondiffpatch@0.7.3: + dependencies: + '@dmsnell/diff-match-patch': 1.1.0 + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + knip@5.61.3(@types/node@22.16.0)(typescript@5.8.3): + dependencies: + '@nodelib/fs.walk': 1.2.8 + '@types/node': 22.16.0 + fast-glob: 3.3.3 + formatly: 0.2.4 + jiti: 2.4.2 + js-yaml: 4.1.0 + minimist: 1.2.8 + oxc-resolver: 11.4.0 + picocolors: 1.1.1 + picomatch: 4.0.2 + smol-toml: 1.4.1 + strip-json-comments: 5.0.2 + typescript: 5.8.3 + zod: 3.25.76 + zod-validation-error: 3.5.3(zod@3.25.76) + + konva@9.3.22: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + lines-and-columns@1.2.4: {} + + linkify-react@4.3.1(linkifyjs@4.3.1)(react@18.3.1): + dependencies: + linkifyjs: 4.3.1 + react: 18.3.1 + + linkifyjs@4.3.1: {} + + liqe@3.8.3: + dependencies: + nearley: 2.20.1 + ts-error: 1.0.6 + + load-tsconfig@0.2.5: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash.merge@4.6.2: {} + + lodash.mergewith@4.6.2: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.1.4: {} + + lru-cache@10.4.3: {} + + lru-cache@11.1.0: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.2 + + math-expression-evaluator@2.0.7: {} + + math-intrinsics@1.1.0: {} + + mdn-data@2.0.14: {} + + memoize-one@6.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-fn@2.1.0: {} + + min-indent@1.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + moo@0.5.2: {} + + motion-dom@11.18.1: + dependencies: + motion-utils: 11.18.1 + + motion-utils@11.18.1: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + mtwist@1.0.2: {} + + nano-css@5.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + css-tree: 1.1.3 + csstype: 3.1.3 + fastest-stable-stringify: 2.0.2 + inline-style-prefixer: 7.0.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + rtl-css-js: 1.16.1 + stacktrace-js: 2.0.2 + stylis: 4.3.6 + + nanoid@3.3.11: {} + + nanoid@5.1.5: {} + + nanostores@1.0.1: {} + + natural-compare@1.4.0: {} + + nearley@2.20.1: + dependencies: + commander: 2.20.3 + moo: 0.5.2 + railroad-diagrams: 1.0.0 + randexp: 0.4.6 + + new-github-issue-url@1.1.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-releases@2.0.19: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + openapi-types@12.1.3: {} + + openapi-typescript@7.8.0(typescript@5.8.3): + dependencies: + '@redocly/openapi-core': 1.34.3(supports-color@10.0.0) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.0.0 + typescript: 5.8.3 + yargs-parser: 21.1.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + overlayscrollbars-react@0.5.6(overlayscrollbars@2.11.4)(react@18.3.1): + dependencies: + overlayscrollbars: 2.11.4 + react: 18.3.1 + + overlayscrollbars-react@0.5.6(overlayscrollbars@2.12.0)(react@18.3.1): + dependencies: + overlayscrollbars: 2.12.0 + react: 18.3.1 + + overlayscrollbars@2.11.4: {} + + overlayscrollbars@2.12.0: {} + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + oxc-resolver@11.4.0: + optionalDependencies: + '@oxc-resolver/binding-darwin-arm64': 11.4.0 + '@oxc-resolver/binding-darwin-x64': 11.4.0 + '@oxc-resolver/binding-freebsd-x64': 11.4.0 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.4.0 + '@oxc-resolver/binding-linux-arm64-gnu': 11.4.0 + '@oxc-resolver/binding-linux-arm64-musl': 11.4.0 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.4.0 + '@oxc-resolver/binding-linux-s390x-gnu': 11.4.0 + '@oxc-resolver/binding-linux-x64-gnu': 11.4.0 + '@oxc-resolver/binding-linux-x64-musl': 11.4.0 + '@oxc-resolver/binding-wasm32-wasi': 11.4.0 + '@oxc-resolver/binding-win32-arm64-msvc': 11.4.0 + '@oxc-resolver/binding-win32-x64-msvc': 11.4.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.1 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + package-json-from-dist@1.0.1: {} + + pako@1.0.11: {} + + pako@2.1.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.27.1 + index-to-position: 1.1.0 + type-fest: 4.41.0 + + path-exists@4.0.0: {} + + path-exists@5.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + perfect-freehand@1.2.2: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + picomatch@4.0.3: {} + + pluralize@8.0.0: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.6.2: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + process-nextick-args@2.0.1: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + query-string@9.2.2: + dependencies: + decode-uri-component: 0.4.1 + filter-obj: 5.1.0 + split-on-first: 3.0.0 + + queue-microtask@1.2.3: {} + + raf-schd@4.0.3: {} + + raf-throttle@2.0.6: {} + + railroad-diagrams@1.0.0: {} + + randexp@0.4.6: + dependencies: + discontinuous-range: 1.0.0 + ret: 0.1.15 + + react-clientside-effect@1.2.8(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.3 + react: 18.3.1 + + react-colorful@5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-docgen-typescript@2.4.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + react-docgen@8.0.0: + dependencies: + '@babel/core': 7.28.0 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.7 + '@types/doctrine': 0.0.9 + '@types/resolve': 1.20.6 + doctrine: 3.0.0 + resolve: 1.22.10 + strip-indent: 4.0.0 + transitivePeerDependencies: + - supports-color + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-dropzone@14.3.8(react@18.3.1): + dependencies: + attr-accept: 2.2.5 + file-selector: 2.1.2 + prop-types: 15.8.1 + react: 18.3.1 + + react-error-boundary@5.0.0(react@18.3.1): + dependencies: + '@babel/runtime': 7.27.6 + react: 18.3.1 + + react-fast-compare@3.2.2: {} + + react-focus-lock@2.13.6(@types/react@18.3.23)(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.3 + focus-lock: 1.3.6 + prop-types: 15.8.1 + react: 18.3.1 + react-clientside-effect: 1.2.8(react@18.3.1) + use-callback-ref: 1.3.3(@types/react@18.3.23)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + + react-hook-form@7.60.0(react@18.3.1): + dependencies: + react: 18.3.1 + + react-hotkeys-hook@4.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-i18next@15.6.0(i18next@25.3.2(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3): + dependencies: + '@babel/runtime': 7.27.6 + html-parse-stringify: 3.0.1 + i18next: 25.3.2(typescript@5.8.3) + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + typescript: 5.8.3 + + react-i18next@15.7.3(i18next@25.3.2(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3): + dependencies: + '@babel/runtime': 7.28.3 + html-parse-stringify: 3.0.1 + i18next: 25.3.2(typescript@5.8.3) + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + typescript: 5.8.3 + + react-icons@5.5.0(react@18.3.1): + dependencies: + react: 18.3.1 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-redux@9.2.0(@types/react@18.3.23)(react@18.3.1)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 18.3.1 + use-sync-external-store: 1.5.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + redux: 5.0.1 + + react-remove-scroll-bar@2.3.8(@types/react@18.3.23)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.23)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.23 + + react-remove-scroll@2.7.1(@types/react@18.3.23)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.23)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.23)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.23)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + + react-resizable-panels@3.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-router-dom@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + cookie: 1.1.1 + react: 18.3.1 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + react-select@5.10.2(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.3 + '@emotion/cache': 11.14.0 + '@emotion/react': 11.14.0(@types/react@18.3.23)(react@18.3.1) + '@floating-ui/dom': 1.7.2 + '@types/react-transition-group': 4.4.12(@types/react@18.3.23) + memoize-one: 6.0.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + use-isomorphic-layout-effect: 1.2.1(@types/react@18.3.23)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - supports-color + + react-select@5.8.3(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.27.6 + '@emotion/cache': 11.14.0 + '@emotion/react': 11.14.0(@types/react@18.3.23)(react@18.3.1) + '@floating-ui/dom': 1.7.2 + '@types/react-transition-group': 4.4.12(@types/react@18.3.23) + memoize-one: 6.0.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + use-isomorphic-layout-effect: 1.2.1(@types/react@18.3.23)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - supports-color + + react-style-singleton@2.2.3(@types/react@18.3.23)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.23 + + react-textarea-autosize@8.5.9(@types/react@18.3.23)(react@18.3.1): + dependencies: + '@babel/runtime': 7.27.6 + react: 18.3.1 + use-composed-ref: 1.4.0(@types/react@18.3.23)(react@18.3.1) + use-latest: 1.3.0(@types/react@18.3.23)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.27.6 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-universal-interface@0.6.2(react@18.3.1)(tslib@2.8.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + + react-use@17.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@types/js-cookie': 2.2.7 + '@xobotyi/scrollbar-width': 1.9.5 + copy-to-clipboard: 3.3.3 + fast-deep-equal: 3.1.3 + fast-shallow-equal: 1.0.0 + js-cookie: 2.2.1 + nano-css: 5.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-universal-interface: 0.6.2(react@18.3.1)(tslib@2.8.1) + resize-observer-polyfill: 1.5.1 + screenfull: 5.2.0 + set-harmonic-interval: 1.0.1 + throttle-debounce: 3.0.1 + ts-easing: 0.2.0 + tslib: 2.8.1 + + react-virtuoso@4.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + redux-remember@5.2.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux-undo@1.1.0: {} + + redux@5.0.1: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + requireindex@1.1.0: {} + + reselect@5.1.1: {} + + resize-observer-polyfill@1.5.1: {} + + resolve-from@4.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + ret@0.1.15: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + roarr@7.21.1: + dependencies: + fast-printf: 1.6.10 + safe-stable-stringify: 2.5.0 + semver-compare: 1.0.0 + + rollup-plugin-visualizer@6.0.3(rollup@4.45.1): + dependencies: + open: 8.4.2 + picomatch: 4.0.3 + source-map: 0.7.4 + yargs: 17.7.2 + optionalDependencies: + rollup: 4.45.1 + + rollup@2.79.2: + optionalDependencies: + fsevents: 2.3.3 + + rollup@4.45.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.45.1 + '@rollup/rollup-android-arm64': 4.45.1 + '@rollup/rollup-darwin-arm64': 4.45.1 + '@rollup/rollup-darwin-x64': 4.45.1 + '@rollup/rollup-freebsd-arm64': 4.45.1 + '@rollup/rollup-freebsd-x64': 4.45.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.45.1 + '@rollup/rollup-linux-arm-musleabihf': 4.45.1 + '@rollup/rollup-linux-arm64-gnu': 4.45.1 + '@rollup/rollup-linux-arm64-musl': 4.45.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.45.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.45.1 + '@rollup/rollup-linux-riscv64-gnu': 4.45.1 + '@rollup/rollup-linux-riscv64-musl': 4.45.1 + '@rollup/rollup-linux-s390x-gnu': 4.45.1 + '@rollup/rollup-linux-x64-gnu': 4.45.1 + '@rollup/rollup-linux-x64-musl': 4.45.1 + '@rollup/rollup-win32-arm64-msvc': 4.45.1 + '@rollup/rollup-win32-ia32-msvc': 4.45.1 + '@rollup/rollup-win32-x64-msvc': 4.45.1 + fsevents: 2.3.3 + + rtl-css-js@1.16.1: + dependencies: + '@babel/runtime': 7.27.6 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-stable-stringify@2.5.0: {} + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + screenfull@5.2.0: {} + + semver-compare@1.0.0: {} + + semver@6.3.1: {} + + semver@7.7.2: {} + + serialize-error@12.0.0: + dependencies: + type-fest: 4.41.0 + + set-cookie-parser@2.7.2: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-harmonic-interval@1.0.1: {} + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + setimmediate@1.0.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sirv@3.0.1: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + smol-toml@1.4.1: {} + + socket.io-client@4.8.1: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + engine.io-client: 6.6.3 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + source-map-js@1.2.1: {} + + source-map@0.5.6: {} + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + split-on-first@3.0.0: {} + + stable-hash@0.0.6: {} + + stack-generator@2.0.10: + dependencies: + stackframe: 1.3.4 + + stackback@0.0.2: {} + + stackframe@1.3.4: {} + + stacktrace-gps@3.1.2: + dependencies: + source-map: 0.5.6 + stackframe: 1.3.4 + + stacktrace-js@2.0.2: + dependencies: + error-stack-parser: 2.1.4 + stack-generator: 2.0.10 + stacktrace-gps: 3.1.2 + + std-env@3.9.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + storybook@9.0.17(@testing-library/dom@10.4.0)(prettier@3.6.2): + dependencies: + '@storybook/global': 5.0.0 + '@testing-library/jest-dom': 6.6.3 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) + '@vitest/expect': 3.2.4 + '@vitest/spy': 3.2.4 + better-opn: 3.0.2 + esbuild: 0.25.6 + esbuild-register: 3.6.0(esbuild@0.25.6) + recast: 0.23.11 + semver: 7.7.2 + ws: 8.18.3 + optionalDependencies: + prettier: 3.6.2 + transitivePeerDependencies: + - '@testing-library/dom' + - bufferutil + - supports-color + - utf-8-validate + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.0 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-bom@3.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-indent@4.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@3.1.1: {} + + strip-json-comments@5.0.2: {} + + strip-literal@3.0.0: + dependencies: + js-tokens: 9.0.1 + + stylis@4.2.0: {} + + stylis@4.3.6: {} + + supports-color@10.0.0: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + test-exclude@7.0.1: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.4.5 + minimatch: 9.0.5 + + throttle-debounce@3.0.1: {} + + tiny-invariant@1.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.3: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toggle-selection@1.0.6: {} + + totalist@3.0.1: {} + + tr46@0.0.3: {} + + tree-kill@1.2.2: {} + + ts-api-utils@2.1.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + ts-dedent@2.2.0: {} + + ts-easing@0.2.0: {} + + ts-error@1.0.6: {} + + tsafe@1.8.5: {} + + tsconfck@3.1.6(typescript@5.8.3): + optionalDependencies: + typescript: 5.8.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.4.0: {} + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@4.41.0: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.8.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + unicorn-magic@0.1.0: {} + + universalify@2.0.1: {} + + unplugin@1.16.1: + dependencies: + acorn: 8.15.0 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.1.3(browserslist@4.25.1): + dependencies: + browserslist: 4.25.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js-replace@1.0.1: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@18.3.23)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.23 + + use-composed-ref@1.4.0(@types/react@18.3.23)(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + use-debounce@10.0.5(react@18.3.1): + dependencies: + react: 18.3.1 + + use-device-pixel-ratio@1.1.2(react@18.3.1): + dependencies: + react: 18.3.1 + + use-isomorphic-layout-effect@1.2.1(@types/react@18.3.23)(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + use-latest@1.3.0(@types/react@18.3.23)(react@18.3.1): + dependencies: + react: 18.3.1 + use-isomorphic-layout-effect: 1.2.1(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + + use-sidecar@1.1.3(@types/react@18.3.23)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.23 + + use-sync-external-store@1.5.0(react@18.3.1): + dependencies: + react: 18.3.1 + + util-deprecate@1.0.2: {} + + uuid@11.1.0: {} + + vite-node@3.2.4(@types/node@22.16.0)(jiti@2.4.2): + dependencies: + cac: 6.7.14 + debug: 4.4.1(supports-color@10.0.0) + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.0.5(@types/node@22.16.0)(jiti@2.4.2) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-plugin-eslint@1.8.1(eslint@9.31.0(jiti@2.4.2))(vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2)): + dependencies: + '@rollup/pluginutils': 4.2.1 + '@types/eslint': 8.56.12 + eslint: 9.31.0(jiti@2.4.2) + rollup: 2.79.2 + vite: 7.0.5(@types/node@22.16.0)(jiti@2.4.2) + + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2)): + dependencies: + debug: 4.4.1(supports-color@10.0.0) + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.8.3) + optionalDependencies: + vite: 7.0.5(@types/node@22.16.0)(jiti@2.4.2) + transitivePeerDependencies: + - supports-color + - typescript + + vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2): + dependencies: + esbuild: 0.25.6 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.45.1 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 22.16.0 + fsevents: 2.3.3 + jiti: 2.4.2 + + vitest@3.2.4(@types/node@22.16.0)(@vitest/ui@3.2.4)(jiti@2.4.2): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.0.5(@types/node@22.16.0)(jiti@2.4.2)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.0 + debug: 4.4.1(supports-color@10.0.0) + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.0.5(@types/node@22.16.0)(jiti@2.4.2) + vite-node: 3.2.4(@types/node@22.16.0)(jiti@2.4.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.16.0 + '@vitest/ui': 3.2.4(vitest@3.2.4) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + void-elements@3.1.0: {} + + walk-up-path@4.0.0: {} + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webidl-conversions@3.0.1: {} + + webpack-virtual-modules@0.6.2: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + ws@8.17.1: {} + + ws@8.18.3: {} + + xmlhttprequest-ssl@2.1.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml-ast-parser@0.0.43: {} + + yaml@1.10.2: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.1: {} + + zod-validation-error@3.5.3(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod-validation-error@3.5.3(zod@4.0.10): + dependencies: + zod: 4.0.10 + + zod@3.25.76: {} + + zod@4.0.10: {} + + zustand@4.5.7(@types/react@18.3.23)(immer@10.1.1)(react@18.3.1): + dependencies: + use-sync-external-store: 1.5.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + immer: 10.1.1 + react: 18.3.1 diff --git a/invokeai/frontend/web/pnpm-workspace.yaml b/invokeai/frontend/web/pnpm-workspace.yaml new file mode 100644 index 00000000000..7c326294a5e --- /dev/null +++ b/invokeai/frontend/web/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +onlyBuiltDependencies: + - '@swc/core' + - esbuild diff --git a/invokeai/frontend/web/public/assets/images/commercial-license-bg.png b/invokeai/frontend/web/public/assets/images/commercial-license-bg.png new file mode 100644 index 00000000000..a5e8c3a0029 Binary files /dev/null and b/invokeai/frontend/web/public/assets/images/commercial-license-bg.png differ diff --git a/invokeai/frontend/web/public/assets/images/denoising-strength.png b/invokeai/frontend/web/public/assets/images/denoising-strength.png new file mode 100644 index 00000000000..b286298a5d8 Binary files /dev/null and b/invokeai/frontend/web/public/assets/images/denoising-strength.png differ diff --git a/invokeai/frontend/web/public/assets/images/invoke-alert-favicon.svg b/invokeai/frontend/web/public/assets/images/invoke-alert-favicon.svg new file mode 100644 index 00000000000..d0a40b9f011 --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-alert-favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-avatar-circle.svg b/invokeai/frontend/web/public/assets/images/invoke-avatar-circle.svg new file mode 100755 index 00000000000..73221cabf3a --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-avatar-circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-avatar-square.svg b/invokeai/frontend/web/public/assets/images/invoke-avatar-square.svg new file mode 100755 index 00000000000..1470b8cb79d --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-avatar-square.svg @@ -0,0 +1,4 @@ + + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-favicon.png b/invokeai/frontend/web/public/assets/images/invoke-favicon.png new file mode 100755 index 00000000000..f9fa13c242f Binary files /dev/null and b/invokeai/frontend/web/public/assets/images/invoke-favicon.png differ diff --git a/invokeai/frontend/web/public/assets/images/invoke-favicon.svg b/invokeai/frontend/web/public/assets/images/invoke-favicon.svg new file mode 100755 index 00000000000..b1daa84f45b --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-key-char-lrg.svg b/invokeai/frontend/web/public/assets/images/invoke-key-char-lrg.svg new file mode 100755 index 00000000000..2df569b9397 --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-key-char-lrg.svg @@ -0,0 +1,4 @@ + + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-key-char-sml.svg b/invokeai/frontend/web/public/assets/images/invoke-key-char-sml.svg new file mode 100755 index 00000000000..a280e5a67bd --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-key-char-sml.svg @@ -0,0 +1,4 @@ + + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-key-wht-lrg.svg b/invokeai/frontend/web/public/assets/images/invoke-key-wht-lrg.svg new file mode 100755 index 00000000000..91f210c82bf --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-key-wht-lrg.svg @@ -0,0 +1,4 @@ + + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-key-wht-sml.svg b/invokeai/frontend/web/public/assets/images/invoke-key-wht-sml.svg new file mode 100755 index 00000000000..ef27d397bf8 --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-key-wht-sml.svg @@ -0,0 +1,4 @@ + + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-symbol-char-lrg.svg b/invokeai/frontend/web/public/assets/images/invoke-symbol-char-lrg.svg new file mode 100755 index 00000000000..067a22386b4 --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-symbol-char-lrg.svg @@ -0,0 +1,3 @@ + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-symbol-char-sml.svg b/invokeai/frontend/web/public/assets/images/invoke-symbol-char-sml.svg new file mode 100755 index 00000000000..6ea2abfb6fd --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-symbol-char-sml.svg @@ -0,0 +1,3 @@ + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-symbol-wht-lrg.svg b/invokeai/frontend/web/public/assets/images/invoke-symbol-wht-lrg.svg new file mode 100755 index 00000000000..17cfdc77da7 --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-symbol-wht-lrg.svg @@ -0,0 +1,3 @@ + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-symbol-wht-sml.svg b/invokeai/frontend/web/public/assets/images/invoke-symbol-wht-sml.svg new file mode 100755 index 00000000000..bb2d62e21ac --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-symbol-wht-sml.svg @@ -0,0 +1,3 @@ + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-symbol-ylw-lrg.svg b/invokeai/frontend/web/public/assets/images/invoke-symbol-ylw-lrg.svg new file mode 100644 index 00000000000..898f20bd6fd --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-symbol-ylw-lrg.svg @@ -0,0 +1,3 @@ + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-tag-char-lrg.svg b/invokeai/frontend/web/public/assets/images/invoke-tag-char-lrg.svg new file mode 100644 index 00000000000..9256fb87d1c --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-tag-char-lrg.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-tag-char-sml.svg b/invokeai/frontend/web/public/assets/images/invoke-tag-char-sml.svg new file mode 100644 index 00000000000..7d5b2846d04 --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-tag-char-sml.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-tag-lrg.svg b/invokeai/frontend/web/public/assets/images/invoke-tag-lrg.svg new file mode 100644 index 00000000000..43c435cc5c6 --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-tag-lrg.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-tag-sml.svg b/invokeai/frontend/web/public/assets/images/invoke-tag-sml.svg new file mode 100644 index 00000000000..2a31641a137 --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-tag-sml.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-wordmark-charcoal.svg b/invokeai/frontend/web/public/assets/images/invoke-wordmark-charcoal.svg new file mode 100644 index 00000000000..a700e0c00f4 --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-wordmark-charcoal.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/invokeai/frontend/web/public/assets/images/invoke-wordmark-white.svg b/invokeai/frontend/web/public/assets/images/invoke-wordmark-white.svg new file mode 100644 index 00000000000..9f5d216cf7c --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/invoke-wordmark-white.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/invokeai/frontend/web/public/assets/images/mask.svg b/invokeai/frontend/web/public/assets/images/mask.svg new file mode 100644 index 00000000000..8cc4bee4242 --- /dev/null +++ b/invokeai/frontend/web/public/assets/images/mask.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/invokeai/frontend/web/public/locales/ar.json b/invokeai/frontend/web/public/locales/ar.json new file mode 100644 index 00000000000..0a03deb6472 --- /dev/null +++ b/invokeai/frontend/web/public/locales/ar.json @@ -0,0 +1,83 @@ +{ + "common": { + "hotkeysLabel": "مفاتيح الأختصار", + "languagePickerLabel": "منتقي اللغة", + "reportBugLabel": "بلغ عن خطأ", + "settingsLabel": "إعدادات", + "img2img": "صورة إلى صورة", + "nodes": "عقد", + "upload": "رفع", + "load": "تحميل", + "back": "الى الخلف", + "statusDisconnected": "غير متصل" + }, + "gallery": { + "galleryImageSize": "حجم الصورة", + "gallerySettings": "إعدادات المعرض", + "autoSwitchNewImages": "التبديل التلقائي إلى الصور الجديدة" + }, + "modelManager": { + "modelManager": "مدير النموذج", + "model": "نموذج", + "allModels": "جميع النماذج", + "modelUpdated": "تم تحديث النموذج", + "manual": "يدوي", + "name": "الاسم", + "description": "الوصف", + "config": "تكوين", + "repo_id": "معرف المستودع", + "width": "عرض", + "height": "ارتفاع", + "addModel": "أضف نموذج", + "availableModels": "النماذج المتاحة", + "search": "بحث", + "load": "تحميل", + "active": "نشط", + "selected": "تم التحديد", + "delete": "حذف", + "deleteModel": "حذف النموذج", + "deleteConfig": "حذف التكوين", + "deleteMsg1": "هل أنت متأكد من رغبتك في حذف إدخال النموذج هذا من استحضر الذكاء الصناعي", + "deleteMsg2": "هذا لن يحذف ملف نقطة التحكم للنموذج من القرص الخاص بك. يمكنك إعادة إضافتهم إذا كنت ترغب في ذلك." + }, + "parameters": { + "images": "الصور", + "steps": "الخطوات", + "cfgScale": "مقياس الإعداد الذاتي للجملة", + "width": "عرض", + "height": "ارتفاع", + "seed": "بذرة", + "shuffle": "تشغيل", + "noiseThreshold": "عتبة الضوضاء", + "perlinNoise": "ضجيج برلين", + "type": "نوع", + "strength": "قوة", + "upscaling": "تصغير", + "scale": "مقياس", + "imageFit": "ملائمة الصورة الأولية لحجم الخرج", + "scaleBeforeProcessing": "تحجيم قبل المعالجة", + "scaledWidth": "العرض المحجوب", + "scaledHeight": "الارتفاع المحجوب", + "infillMethod": "طريقة التعبئة", + "tileSize": "حجم البلاطة", + "copyImage": "نسخ الصورة", + "usePrompt": "استخدم المحث", + "useSeed": "استخدام البذور", + "useAll": "استخدام الكل", + "info": "معلومات" + }, + "settings": { + "models": "موديلات", + "displayInProgress": "عرض الصور المؤرشفة", + "confirmOnDelete": "تأكيد عند الحذف", + "resetWebUI": "إعادة تعيين واجهة الويب", + "resetWebUIDesc1": "إعادة تعيين واجهة الويب يعيد فقط ذاكرة التخزين المؤقت للمتصفح لصورك وإعداداتك المذكورة. لا يحذف أي صور من القرص.", + "resetWebUIDesc2": "إذا لم تظهر الصور في الصالة أو إذا كان شيء آخر غير ناجح، يرجى المحاولة إعادة تعيين قبل تقديم مشكلة على جيت هب.", + "resetComplete": "تم إعادة تعيين واجهة الويب. تحديث الصفحة لإعادة التحميل." + }, + "toast": { + "uploadFailed": "فشل التحميل", + "imageCopied": "تم نسخ الصورة", + "parametersNotSet": "لم يتم تعيين المعلمات" + } +} diff --git a/invokeai/frontend/web/public/locales/az.json b/invokeai/frontend/web/public/locales/az.json new file mode 100644 index 00000000000..54c65ff2916 --- /dev/null +++ b/invokeai/frontend/web/public/locales/az.json @@ -0,0 +1,5 @@ +{ + "accessibility": { + "about": "Haqqında" + } +} diff --git a/invokeai/frontend/web/public/locales/bg.json b/invokeai/frontend/web/public/locales/bg.json new file mode 100644 index 00000000000..51070b46fe1 --- /dev/null +++ b/invokeai/frontend/web/public/locales/bg.json @@ -0,0 +1,60 @@ +{ + "accessibility": { + "menu": "Меню", + "nextImage": "Следваща снимка", + "previousImage": "Предишно изображение", + "uploadImage": "Качете изображение", + "invokeProgressBar": "Invoke лента за напредък", + "mode": "Режим" + }, + "boards": { + "addBoard": "Добавете табло", + "cancel": "Отказ", + "autoAddBoard": "Авто-добавяне на табло", + "changeBoard": "Смяна на табло", + "deleteBoard": "Изтриване на табло", + "deleteBoardAndImages": "Изтриване на табло и изображения", + "deleteBoardOnly": "Изтриване само на таблото", + "loading": "Зареждане...", + "movingImagesToBoard_one": "Преместване на {{count}} снимка към табло:", + "movingImagesToBoard_other": "Преместване на {{count}} снимки към табло:", + "selectBoard": "Изберете табло", + "uncategorized": "Некатегоризирани", + "downloadBoard": "Изтегляне на табло", + "bottomMessage": "Изтриването на това табло и изображенията в него ще нулира всички функции, които ги използват в момента.", + "deletedBoardsCannotbeRestored": "Изтритите табла не могат да бъдат възстановени", + "myBoard": "Моето табло" + }, + "accordions": { + "image": { + "title": "Изображение" + }, + "control": { + "title": "Контрол" + } + }, + "common": { + "aboutDesc": "Използвате Invoke за работа? Разгледайте:", + "ai": "ии", + "areYouSure": "Сигурен ли сте?", + "back": "Назад", + "cancel": "Отказ", + "or": "или", + "controlNet": "ControlNet", + "details": "Детайли", + "dontAskMeAgain": "Не питай повече", + "folder": "Папка", + "githubLabel": "Github", + "img2img": "Снимка към снимка", + "languagePickerLabel": "Език", + "loading": "Зареждане", + "learnMore": "Научете повече", + "modelManager": "Мениджър на модели", + "openInNewTab": "Отворете в нов таб", + "orderBy": "Подреждане по", + "communityLabel": "Общност", + "discordLabel": "Дискорд", + "error": "Грешка", + "file": "Файл" + } +} diff --git a/invokeai/frontend/web/public/locales/de.json b/invokeai/frontend/web/public/locales/de.json new file mode 100644 index 00000000000..6562294631f --- /dev/null +++ b/invokeai/frontend/web/public/locales/de.json @@ -0,0 +1,1769 @@ +{ + "common": { + "languagePickerLabel": "Sprachauswahl", + "reportBugLabel": "Fehler melden", + "settingsLabel": "Einstellungen", + "img2img": "Bild zu Bild", + "nodes": "Arbeitsabläufe", + "upload": "Hochladen", + "load": "Laden", + "statusDisconnected": "Getrennt", + "cancel": "Abbrechen", + "accept": "Annehmen", + "back": "Zurück", + "hotkeysLabel": "Tastenkombinationen", + "githubLabel": "Github", + "discordLabel": "Discord", + "txt2img": "Text zu Bild", + "postprocessing": "Nachbearbeitung", + "t2iAdapter": "T2I Adapter", + "communityLabel": "Gemeinschaft", + "dontAskMeAgain": "Nicht nochmal fragen", + "areYouSure": "Bist du sicher?", + "on": "An", + "ipAdapter": "IP Adapter", + "auto": "Auto", + "controlNet": "ControlNet", + "modelManager": "Model Manager", + "learnMore": "Mehr erfahren", + "loading": "Lade", + "random": "Zufall", + "batch": "Stapel-Manager", + "advanced": "Erweitert", + "openInNewTab": "In einem neuem Tab öffnen", + "linear": "Linear", + "checkpoint": "Checkpoint", + "inpaint": "Inpaint", + "simple": "Einfach", + "template": "Vorlage", + "outputs": "Ausgabe", + "data": "Daten", + "safetensors": "Safe-Tensors", + "outpaint": "Outpaint", + "details": "Details", + "format": "Format", + "unknown": "Unbekannt", + "folder": "Ordner", + "error": "Fehler", + "installed": "Installiert", + "ai": "KI", + "file": "Datei", + "somethingWentWrong": "Etwas ist schief gelaufen", + "copyError": "$t(gallery.copy) Fehler", + "input": "Eingabe", + "alpha": "Alpha", + "red": "Rot", + "green": "Grün", + "blue": "Blau", + "delete": "Löschen", + "or": "oder", + "direction": "Richtung", + "save": "Speichern", + "created": "Erstellt", + "unknownError": "Unbekannter Fehler", + "aboutDesc": "Verwenden Sie Invoke für die Arbeit? Siehe hier:", + "orderBy": "Ordnen nach", + "saveAs": "Speichern als", + "updated": "Aktualisiert", + "copy": "Kopieren", + "aboutHeading": "Nutzen Sie Ihre kreative Energie", + "toResolve": "Lösen", + "add": "Hinzufügen", + "selected": "Ausgewählt", + "beta": "Beta", + "editor": "Editor", + "positivePrompt": "Positiv-Prompt", + "negativePrompt": "Negativ-Prompt", + "tab": "Tabulator", + "enabled": "Aktiviert", + "disabled": "Ausgeschaltet", + "dontShowMeThese": "Zeig mir diese nicht", + "apply": "Anwenden", + "edit": "Ändern", + "openInViewer": "Im Viewer öffnen", + "loadingImage": "Lade Bild", + "off": "Aus", + "view": "Anzeigen", + "placeholderSelectAModel": "Modell auswählen", + "reset": "Zurücksetzen", + "none": "Keine", + "new": "Neu", + "ok": "OK", + "close": "Schließen", + "clipboard": "Zwischenablage", + "generating": "Generieren", + "loadingModel": "Lade Modell", + "warnings": "Warnungen", + "start": "Starten", + "count": "Anzahl", + "step": "Schritt", + "values": "Werte", + "min": "Min", + "max": "Max", + "seed": "Seed", + "row": "Reihe", + "column": "Spalte", + "end": "Ende", + "layout": "Layout", + "board": "Ordner", + "combinatorial": "Kombinatorisch", + "saveChanges": "Änderungen speichern", + "error_withCount_one": "{{count}} Fehler", + "error_withCount_other": "{{count}} Fehler", + "value": "Wert", + "label": "Label", + "systemInformation": "Systeminformationen", + "search": "Suche", + "clear": "Zurücksetzen", + "fullView": "Vollansicht", + "compactView": "Kompaktansicht", + "options_withCount_one": "{{count}} Option", + "options_withCount_other": "{{count}} Optionen", + "noOptions": "Keine Optionen", + "noMatches": "Keine Treffer", + "model_withCount_one": "{{count}} Modell", + "model_withCount_other": "{{count}} Modelle" + }, + "gallery": { + "galleryImageSize": "Bildgröße", + "gallerySettings": "Galerie-Einstellungen", + "autoSwitchNewImages": "Auto-Wechsel zu neuen Bildern", + "loading": "Lade", + "deleteImage_one": "Lösche Bild", + "deleteImage_other": "Lösche {{count}} Bilder", + "copy": "Kopieren", + "download": "Runterladen", + "featuresWillReset": "Wenn Sie dieses Bild löschen, werden diese Funktionen sofort zurückgesetzt.", + "downloadSelection": "Auswahl herunterladen", + "currentlyInUse": "Dieses Bild wird derzeit in den folgenden Funktionen verwendet:", + "deleteImagePermanent": "Gelöschte Bilder können nicht wiederhergestellt werden.", + "autoAssignBoardOnClick": "Board per Klick automatisch zuweisen", + "noImageSelected": "Kein Bild ausgewählt", + "starImage": "Bild markieren", + "unstarImage": "Markierung entfernen", + "image": "Bild", + "deleteSelection": "Lösche Auswahl", + "dropToUpload": "$t(gallery.drop) zum hochladen", + "dropOrUpload": "$t(gallery.drop) oder hochladen", + "drop": "Ablegen", + "bulkDownloadRequested": "Download vorbereiten", + "bulkDownloadRequestedDesc": "Dein Download wird vorbereitet. Dies kann ein paar Momente dauern.", + "bulkDownloadRequestFailed": "Problem beim Download vorbereiten", + "bulkDownloadFailed": "Download fehlgeschlagen", + "alwaysShowImageSizeBadge": "Zeige immer Bilder Größe Abzeichen", + "selectForCompare": "Zum Vergleichen auswählen", + "compareImage": "Bilder vergleichen", + "exitSearch": "Bildsuche beenden", + "newestFirst": "Neueste zuerst", + "oldestFirst": "Älteste zuerst", + "openInViewer": "Im Viewer öffnen", + "swapImages": "Bilder tauschen", + "slider": "Slider", + "showStarredImagesFirst": "Mit * markierte Bilder zuerst zeigen", + "compareHelp1": "Halten Sie Alt gedrückt, während Sie auf ein Galeriebild klicken oder die Pfeiltasten verwenden, um das Vergleichsbild zu ändern.", + "compareHelp4": "Drücken Sie Z oder Esc zum Beenden.", + "move": "Bewegen", + "exitBoardSearch": "Suchen beenden", + "searchImages": "Suche mit Metadaten", + "selectAllOnPage": "Alle auf Seite auswählen", + "showArchivedBoards": "Archivierte Boards anzeigen", + "hover": "Schweben", + "compareHelp2": "Drücken Sie M, um durch alle Vergleichsmodi zu wechseln.", + "compareHelp3": "Drücken Sie C, um die verglichenen Bilder zu wechseln.", + "gallery": "Galerie", + "sortDirection": "Sortierreihenfolge", + "sideBySide": "Nebeneinander", + "viewerImage": "Viewer-Bild", + "exitCompare": "Vergleichen beenden", + "stretchToFit": "Strecken bis es passt", + "displayBoardSearch": "Board durchsuchen", + "displaySearch": "Bild suchen", + "go": "Los", + "assetsTab": "Dateien, die Sie zur Verwendung in Ihren Projekten hochgeladen haben.", + "imagesTab": "Bilder, die Sie in Invoke erstellt und gespeichert haben.", + "boardsSettings": "Ordnereinstellungen", + "imagesSettings": "Galeriebildereinstellungen" + }, + "hotkeys": { + "hotkeys": "Tastaturbefehle", + "noHotkeysFound": "Kein Hotkey gefunden", + "searchHotkeys": "Hotkeys durchsuchen", + "clearSearch": "Suche leeren", + "editMode": "Bearbeitungsmodus", + "viewMode": "Ansichtsmodus", + "editHotkey": "Hotkey bearbeiten", + "resetToDefault": "Auf Standard zurücksetzen", + "resetAll": "Alle auf Standard zurücksetzen", + "enterHotkeys": "Tastenkombination(en) eingeben, mit Komma getrennt", + "save": "Speichern", + "cancel": "Abbrechen", + "modifiers": "Modifikatoren", + "syntaxHelp": "Syntax-Hilfe", + "combineWith": "Kombinieren mit +", + "multipleHotkeys": "Mehrere Hotkeys mit Komma", + "validKeys": "Gültige Tasten", + "help": "Hilfe", + "noHotkeysRecorded": "Noch keine Hotkeys aufgenommen", + "pressKeys": "Tasten drücken...", + "setHotkey": "SETZEN", + "setAnother": "WEITEREN SETZEN", + "removeLastHotkey": "Letzten Hotkey entfernen", + "clearAll": "Alle löschen", + "duplicateWarning": "Dieser Hotkey wurde bereits aufgenommen", + "conflictWarning": "wird bereits von \"{{hotkeyTitle}}\" verwendet", + "thisHotkey": "diesem Hotkey", + "canvas": { + "fitBboxToCanvas": { + "desc": "Skalierung und Positionierung der Ansicht auf Bbox-Größe.", + "title": "Bbox auf Arbeitsfläche skalieren" + }, + "selectBboxTool": { + "title": "Bbox Werkzeug", + "desc": "Bbox Werkzeug auswählen." + }, + "title": "Leinwand", + "selectBrushTool": { + "title": "Pinselwerkzeug", + "desc": "Wählen Sie das Pinselwerkzeug aus." + }, + "decrementToolWidth": { + "title": "Werkzeugbreite verringern", + "desc": "Verringern Sie die Breite des Pinsels oder Radiergummis, je nachdem, welches ausgewählt ist." + }, + "incrementToolWidth": { + "title": "Werkzeugbreite erhöhen", + "desc": "Vergrößern Sie die Breite des Pinsels oder Radiergummis, je nachdem, welches ausgewählt ist." + }, + "selectColorPickerTool": { + "title": "Farbwähler-Werkzeug", + "desc": "Farbwähler-Werkzeug auswählen." + }, + "selectEraserTool": { + "title": "Radiergummi-Werkzeug", + "desc": "Radiergummi-Werkzeug auswählen." + }, + "fitLayersToCanvas": { + "title": "Ebenen an die Leinwand anpassen", + "desc": "Alle sichtbaren Ebenen in der Ansicht einpassen." + }, + "filterSelected": { + "title": "Filter", + "desc": "Gewählte Ebene filtern. Nur bei \"Raster\" und Kontroll-Ebenen." + }, + "transformSelected": { + "title": "Umwandeln", + "desc": "Transformieren Sie die ausgewählte Ebene." + }, + "setZoomTo100Percent": { + "title": "Auf 100 % zoomen", + "desc": "Leinwand-Zoom auf 100 % setzen." + }, + "setZoomTo200Percent": { + "title": "Auf 200 % zoomen", + "desc": "Leinwand-Zoom auf 200 % setzen." + }, + "setZoomTo400Percent": { + "title": "Auf 400 % zoomen", + "desc": "Leinwand-Zoom auf 400 % setzen." + }, + "setZoomTo800Percent": { + "title": "Auf 800 % zoomen", + "desc": "Leinwand-Zoom auf 800 % setzen." + }, + "deleteSelected": { + "title": "Ebene löschen", + "desc": "Ausgewählte Ebene löschen." + }, + "undo": { + "title": "Rückgängig", + "desc": "Letzte Aktion rückgängig machen." + }, + "redo": { + "title": "Wiederholen", + "desc": "Letzte Aktion wiederholen." + }, + "nextEntity": { + "title": "Nächste Ebene", + "desc": "Nächste Ebene in der Liste auswählen." + }, + "resetSelected": { + "title": "Ebene zurücksetzen", + "desc": "Ausgewählte Ebene zurücksetzen. Gilt nur für Malmaske bei \"Inpaint\" und \"Regionaler Führung\"." + }, + "prevEntity": { + "title": "Vorherige Ebene", + "desc": "Vorherige Ebene in der Liste auswählen." + }, + "selectMoveTool": { + "title": "Verschieben-Werkzeug", + "desc": "Verschieben-Werkzeug auswählen." + }, + "selectRectTool": { + "title": "Rechteck-Werkzeug", + "desc": "Rechteck-Werkzeug auswählen." + }, + "selectViewTool": { + "desc": "Wählen Sie das Ansichts-Tool.", + "title": "Ansichts-Tool" + }, + "quickSwitch": { + "title": "Ebenen Schnell-Umschalten", + "desc": "Wechseln Sie zwischen den beiden zuletzt gewählten Ebenen. Wenn eine Ebene mit einem Lesezeichen versehen ist, wird zwischen ihr und der letzten nicht markierten Ebene gewechselt." + }, + "applyFilter": { + "title": "Filter anwenden", + "desc": "Wende den ausstehenden Filter auf die ausgewählte Ebene an." + }, + "cancelFilter": { + "title": "Filter abbrechen", + "desc": "Den ausstehenden Filter abbrechen." + }, + "applyTransform": { + "desc": "Die ausstehende Transformation auf die ausgewählte Ebene anwenden.", + "title": "Transformation anwenden" + }, + "cancelTransform": { + "title": "Transformation abbrechen", + "desc": "Die ausstehende Transformation abbrechen." + } + }, + "viewer": { + "useSize": { + "desc": "Aktuelle Bildgröße als Bbox-Größe verwenden.", + "title": "Maße übernehmen" + }, + "title": "Bildbetrachter", + "toggleViewer": { + "title": "Bildbetrachter anzeigen/ausblenden", + "desc": "Zeigen oder verbergen Sie den Bildbetrachter. Nur auf der Arbeitsflächen-Registerkarte." + }, + "nextComparisonMode": { + "title": "Nächster Vergleichsmodus", + "desc": "Alle Vergleichsmodi durchlaufen." + }, + "swapImages": { + "title": "Vergleichsbilder tauschen", + "desc": "Vergleichs-Bilder tauschen." + }, + "runPostprocessing": { + "title": "Nachbearbeitung ausführen", + "desc": "Ausgewählte Nachbearbeitung/en auf aktuelles Bild anwenden." + }, + "toggleMetadata": { + "title": "Metadaten anzeigen/ausblenden", + "desc": "Zeigen oder verbergen der Metadaten des Bildes." + }, + "recallPrompts": { + "title": "Prompts abrufen", + "desc": "Rufen Sie die positiven und negativen Prompts für das aktuelle Bild ab." + }, + "recallSeed": { + "desc": "Seed für aktuelles Bild abrufen.", + "title": "Seed abrufen" + }, + "loadWorkflow": { + "title": "Lade Arbeitsablauf/Workflow", + "desc": "Laden Sie den gespeicherten Workflow des aktuellen Bildes (falls es einen hat)." + }, + "recallAll": { + "title": "Alle Metadaten abrufen", + "desc": "Alle Metadaten für das aktuelle Bild abrufen." + }, + "remix": { + "desc": "Rufen Sie alle Metadaten außer dem Seed für das aktuelle Bild ab.", + "title": "Remixen" + } + }, + "app": { + "invoke": { + "title": "Invoke", + "desc": "Stellt eine Generierung in die Warteschlange und fügt sie am Ende hinzu." + }, + "invokeFront": { + "title": "Invoke (Front)", + "desc": "Stellt eine Generierung in die Warteschlange und fügt sie am Anfang hinzu." + }, + "cancelQueueItem": { + "title": "Abbrechen", + "desc": "Aktuelles Warteschlangenelement abbrechen." + }, + "clearQueue": { + "title": "Warteschlange löschen", + "desc": "Warteschlange abbrechen und komplett löschen." + }, + "selectUpscalingTab": { + "title": "Wählen Sie die Registerkarte Hochskalieren", + "desc": "Wählt die Registerkarte Hochskalieren." + }, + "selectCanvasTab": { + "desc": "Wählt die Arbeitsflächen-Registerkarte.", + "title": "Wählen Sie die Arbeitsflächen-Registerkarte" + }, + "selectWorkflowsTab": { + "title": "Wählt die Registerkarte Arbeitsabläufe", + "desc": "Wählt die Registerkarte Arbeitsabläufe." + }, + "selectModelsTab": { + "title": "Wählt die Registerkarte Modelle", + "desc": "Wählt die Registerkarte Modelle." + }, + "selectQueueTab": { + "title": "Wählt die Registerkarte Warteschlange", + "desc": "Wählt die Registerkarte Warteschlange." + }, + "focusPrompt": { + "desc": "Bewegt den Cursor-Fokus auf den positiven Prompt.", + "title": "Fokus-Prompt" + }, + "toggleLeftPanel": { + "title": "Linkes Panel ein-/ausblenden", + "desc": "Linke Seite zeigen/verbergen." + }, + "toggleRightPanel": { + "title": "Rechte Seite umschalten", + "desc": "Rechte Seite zeigen/verbergen." + }, + "resetPanelLayout": { + "title": "Layout zurücksetzen", + "desc": "Beide Seiten auf Standard zurücksetzen." + }, + "title": "Anwendung", + "togglePanels": { + "title": "Seiten umschalten", + "desc": "Zeigen oder verbergen Sie beide Panels auf einmal." + } + }, + "gallery": { + "title": "Galerie", + "selectAllOnPage": { + "title": "Alle auf der Seite auswählen", + "desc": "Alle Bilder auf der aktuellen Seite auswählen." + }, + "galleryNavRight": { + "title": "Nach rechts navigieren", + "desc": "Navigieren Sie im Galerieraster nach rechts, und wählen Sie das Bild aus. Wenn es sich um das letzte Bild in der Reihe handelt, gehen Sie zur nächsten Reihe. Wenn Sie sich beim letzten Bild der Seite befinden, gehen Sie zur nächsten Seite." + }, + "galleryNavDownAlt": { + "title": "Nach unten navigieren (Bild vergleichen)", + "desc": "Wie \"Abwärts navigieren\", wählt aber das Vergleichsbild aus und öffnet den Vergleichsmodus, falls er nicht bereits geöffnet ist." + }, + "galleryNavUp": { + "title": "Nach oben navigieren", + "desc": "Navigieren Sie im Galerieraster nach oben, und wählen Sie das Bild aus. Wenn Sie sich oben auf der Seite befinden, gehen Sie zur vorherigen Seite." + }, + "galleryNavDown": { + "title": "Nach unten navigieren", + "desc": "Navigieren Sie im Galerieraster nach unten, und wählen Sie das Bild aus. Wenn Sie sich am Ende der Seite befinden, gehen Sie zur nächsten Seite." + }, + "galleryNavLeft": { + "title": "Nach links navigieren", + "desc": "Navigieren Sie im Galerieraster nach links, und wählen Sie das Bild aus. Wenn Sie sich im ersten Bild der Reihe befinden, gehen Sie zur vorherigen Reihe. Wenn Sie sich beim ersten Bild der Seite befinden, gehen Sie zur vorherigen Seite." + }, + "galleryNavUpAlt": { + "title": "Nach oben navigieren (Bild vergleichen)", + "desc": "Wie „Nach oben navigieren“, wählt aber das Vergleichsbild aus und öffnet den Vergleichsmodus, falls er nicht bereits geöffnet ist." + }, + "galleryNavRightAlt": { + "title": "Nach rechts navigieren (Bild vergleichen)", + "desc": "Wie \"Navigieren nach rechts\", wählt aber das Vergleichsbild aus und öffnet den Vergleichsmodus, falls er nicht bereits geöffnet ist." + }, + "clearSelection": { + "title": "Auswahl aufheben", + "desc": "Aktuelle Auswahl aufheben, falls vorhanden." + }, + "galleryNavLeftAlt": { + "title": "Nach links navigieren (Bild vergleichen)", + "desc": "Wie „Nach links navigieren“, wählt aber das Vergleichsbild aus und öffnet den Vergleichsmodus, falls er nicht bereits geöffnet ist." + }, + "deleteSelection": { + "title": "Löschen", + "desc": "Alle ausgewählten Bilder löschen. Standardmäßig werden Sie aufgefordert, den Löschvorgang zu bestätigen. Wenn die Bilder derzeit in der App verwendet werden, werden Sie gewarnt." + } + }, + "workflows": { + "redo": { + "title": "Wiederholen", + "desc": "Letzte Workflow-Aktion wiederherstellen." + }, + "copySelection": { + "title": "Kopieren", + "desc": "Ausgewählte Knoten und Kanten kopieren." + }, + "title": "Arbeitsabläufe", + "addNode": { + "title": "Knoten hinzufügen", + "desc": "Öffnen Sie das \"Knoten zufügen\"-Menü." + }, + "pasteSelection": { + "title": "Einfügen", + "desc": "Kopierte Knoten und Kanten einfügen." + }, + "selectAll": { + "title": "Alles auswählen", + "desc": "Alle Knoten und Kanten auswählen." + }, + "deleteSelection": { + "title": "Löschen", + "desc": "Lösche ausgewählte Knoten und Kanten." + }, + "undo": { + "title": "Rückgängig", + "desc": "Letzte Workflow-Aktion rückgängig machen." + }, + "pasteSelectionWithEdges": { + "desc": "Kopierte Knoten, Kanten und alle mit den kopierten Knoten verbundenen Kanten einfügen.", + "title": "Einfügen mit Kanten" + } + } + }, + "modelManager": { + "modelUpdated": "Model aktualisiert", + "description": "Beschreibung", + "config": "Konfiguration", + "width": "Breite", + "height": "Höhe", + "addModel": "Modell hinzufügen", + "availableModels": "Verfügbare Modelle", + "search": "Suche", + "load": "Laden", + "active": "Aktiv", + "selected": "Ausgewählt", + "delete": "Löschen", + "deleteModel": "Model löschen", + "deleteConfig": "Konfiguration löschen", + "deleteMsg1": "Möchten Sie diesen Model-Eintrag wirklich aus InvokeAI löschen?", + "deleteMsg2": "Dadurch WIRD das Modell von der Festplatte gelöscht WENN es im InvokeAI Root Ordner liegt. Wenn es in einem anderem Ordner liegt wird das Modell NICHT von der Festplatte gelöscht.", + "convert": "Umwandeln", + "allModels": "Alle Modelle", + "alpha": "Alpha", + "convertToDiffusersHelpText2": "Bei diesem Vorgang wird Ihr Eintrag im Modell-Manager durch die Diffusor-Version desselben Modells ersetzt.", + "convertToDiffusersHelpText5": "Bitte stellen Sie sicher, dass Sie über genügend Speicherplatz verfügen. Die Modelle sind in der Regel zwischen 2 GB und 7 GB groß.", + "convertToDiffusersHelpText3": "Ihre Kontrollpunktdatei auf der Festplatte wird NICHT gelöscht oder in irgendeiner Weise verändert. Sie können Ihren Kontrollpunkt dem Modell-Manager wieder hinzufügen, wenn Sie dies wünschen.", + "convertToDiffusersHelpText4": "Dies ist ein einmaliger Vorgang. Er kann je nach den Spezifikationen Ihres Computers etwa 30-60 Sekunden dauern.", + "convertToDiffusersHelpText6": "Möchten Sie dieses Modell konvertieren?", + "modelConverted": "Modell umgewandelt", + "manual": "Manuell", + "modelManager": "Modell Manager", + "model": "Modell", + "name": "Name", + "none": "Nix", + "advanced": "Erweitert", + "convertingModelBegin": "Konvertiere Modell. Bitte warten.", + "baseModel": "Basis Modell", + "convertToDiffusers": "Konvertiere zu Diffusers", + "vae": "VAE", + "predictionType": "Vorhersagetyp", + "selectModel": "Wählen Sie Modell aus", + "repo_id": "Repo-ID", + "modelDeleted": "Modell gelöscht", + "modelUpdateFailed": "Modellaktualisierung fehlgeschlagen", + "settings": "Einstellungen", + "modelConversionFailed": "Modellkonvertierung fehlgeschlagen", + "syncModels": "Modelle synchronisieren", + "modelType": "Modelltyp", + "convertToDiffusersHelpText1": "Dieses Modell wird in das 🧨 Diffusers-Format konvertiert.", + "vaePrecision": "VAE-Präzision", + "variant": "Variante", + "modelDeleteFailed": "Modell konnte nicht gelöscht werden", + "noModelSelected": "Kein Modell ausgewählt", + "huggingFace": "HuggingFace", + "defaultSettings": "Standardeinstellungen", + "edit": "Bearbeiten", + "cancel": "Stornieren", + "defaultSettingsSaved": "Standardeinstellungen gespeichert", + "addModels": "Model hinzufügen", + "deleteModelImage": "Lösche Model Bild", + "huggingFaceRepoID": "HuggingFace Repo ID", + "huggingFacePlaceholder": "besitzer/model-name", + "modelSettings": "Modelleinstellungen", + "typePhraseHere": "Phrase hier eingeben", + "spandrelImageToImage": "Bild zu Bild (Spandrel)", + "starterModels": "Einstiegsmodelle", + "t5Encoder": "T5-Kodierer", + "uploadImage": "Bild hochladen", + "urlOrLocalPath": "URL oder lokaler Pfad", + "install": "Installieren", + "textualInversions": "Textuelle Inversionen", + "modelImageUpdated": "Modellbild aktualisiert", + "path": "Pfad", + "pathToConfig": "Pfad zur Konfiguration", + "scanPlaceholder": "Pfad zu einem lokalen Ordner", + "noMatchingModels": "Keine passenden Modelle", + "localOnly": "nur lokal", + "installAll": "Alles installieren", + "main": "Haupt", + "metadata": "Metadaten", + "modelImageDeleted": "Modellbild gelöscht", + "modelName": "Modellname", + "noModelsInstalled": "Keine Modelle installiert", + "source": "Quelle", + "simpleModelPlaceholder": "URL oder Pfad zu einem lokalen Datei- oder Diffusers-Ordner", + "imageEncoderModelId": "Bild Encoder Modell ID", + "installRepo": "Repo installieren", + "huggingFaceHelper": "Wenn mehrere Modelle in diesem Repo gefunden werden, werden Sie aufgefordert, eines für die Installation auszuwählen.", + "inplaceInstall": "In-place-Installation", + "modelImageDeleteFailed": "Modellbild konnte nicht gelöscht werden", + "repoVariant": "Repo Variante", + "learnMoreAboutSupportedModels": "Erfahren Sie mehr über die Modelle, die wir unterstützen", + "clipEmbed": "CLIP einbetten", + "noModelsInstalledDesc1": "Installiere Modelle mit dem", + "modelImageUpdateFailed": "Modellbild-Update fehlgeschlagen", + "prune": "Bereinigen", + "loraModels": "LoRAs", + "scanFolder": "Ordner scannen", + "installQueue": "Installations-Warteschlange", + "pruneTooltip": "Abgeschlossene Importe aus Warteschlange entfernen", + "scanResults": "Ergebnisse des Scans", + "urlOrLocalPathHelper": "URLs sollten auf eine einzelne Datei deuten. Lokale Pfade können zusätzlich auch auf einen Ordner für ein einzelnes Diffusers-Modell hinweisen.", + "inplaceInstallDesc": "Installieren Sie Modelle, ohne die Dateien zu kopieren. Wenn Sie das Modell verwenden, wird es direkt von seinem Speicherort geladen. Wenn deaktiviert, werden die Dateien während der Installation in das von Invoke verwaltete Modellverzeichnis kopiert.", + "scanFolderHelper": "Der Ordner wird rekursiv nach Modellen durchsucht. Dies kann bei sehr großen Ordnern etwas dauern.", + "includesNModels": "Enthält {{n}} Modelle und deren Abhängigkeiten", + "starterBundles": "Starterpakete", + "installingXModels_one": "{{count}} Modell wird installiert", + "installingXModels_other": "{{count}} Modelle werden installiert", + "skippingXDuplicates_one": ", überspringe {{count}} Duplikat", + "skippingXDuplicates_other": ", überspringe {{count}} Duplikate", + "installingModel": "Modell wird installiert", + "loraTriggerPhrases": "LoRA-Auslösephrasen", + "installingBundle": "Bündel wird installiert", + "triggerPhrases": "Auslösephrasen", + "mainModelTriggerPhrases": "Hauptmodell-Auslösephrasen", + "noDefaultSettings": "Für dieses Modell sind keine Standardeinstellungen konfiguriert. Besuchen Sie den Modell-Manager, um Standardeinstellungen hinzuzufügen.", + "defaultSettingsOutOfSync": "Einige Einstellungen stimmen nicht mit den Standardeinstellungen des Modells überein:", + "clipLEmbed": "CLIP-L einbetten", + "clipGEmbed": "CLIP-G einbetten", + "hfTokenLabel": "HuggingFace-Token (für einige Modelle erforderlich)", + "hfTokenHelperText": "Für die Nutzung einiger Modelle ist ein HF-Token erforderlich. Klicken Sie hier, um Ihr Token zu erstellen oder zu erhalten.", + "hfForbidden": "Sie haben keinen Zugriff auf dieses HF-Modell", + "hfTokenInvalid": "Ungültiges oder fehlendes HF-Token", + "restoreDefaultSettings": "Klicken, um die Standardeinstellungen des Modells zu verwenden.", + "usingDefaultSettings": "Die Standardeinstellungen des Modells werden verwendet", + "hfTokenInvalidErrorMessage": "Ungültiges oder fehlendes HuggingFace-Token.", + "hfTokenUnableToVerify": "HF-Token kann nicht überprüft werden", + "hfTokenUnableToVerifyErrorMessage": "HuggingFace-Token kann nicht überprüft werden. Dies ist wahrscheinlich auf einen Netzwerkfehler zurückzuführen. Bitte versuchen Sie es später erneut.", + "hfTokenSaved": "HF-Token gespeichert", + "hfTokenRequired": "Sie versuchen, ein Modell herunterzuladen, für das ein gültiges HuggingFace-Token erforderlich ist.", + "urlUnauthorizedErrorMessage2": "Hier erfahren wie.", + "urlForbidden": "Sie haben keinen Zugriff auf dieses Modell" + }, + "parameters": { + "images": "Bilder", + "steps": "Schritte", + "cfgScale": "CFG-Skala", + "width": "Breite", + "height": "Höhe", + "shuffle": "Mischen", + "noiseThreshold": "Rausch-Schwellenwert", + "perlinNoise": "Perlin-Rauschen", + "type": "Art", + "strength": "Stärke", + "upscaling": "Hochskalierung", + "scale": "Maßstab", + "imageFit": "Ausgangsbild an Ausgabegröße anpassen", + "scaleBeforeProcessing": "Skalieren vor der Verarbeitung", + "scaledWidth": "Skaliert W", + "scaledHeight": "Skaliert H", + "infillMethod": "Infill-Methode", + "tileSize": "Kachelgröße", + "usePrompt": "Prompt verwenden", + "useSeed": "Seed verwenden", + "useAll": "Alle verwenden", + "copyImage": "Bild kopieren", + "denoisingStrength": "Stärke der Entrauschung", + "symmetry": "Symmetrie", + "info": "Information", + "general": "Allgemein", + "aspect": "Seitenverhältnis", + "scheduler": "Planer", + "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (kann zu groß sein)", + "lockAspectRatio": "Seitenverhältnis sperren", + "swapDimensions": "Seitenverhältnis umkehren", + "setToOptimalSize": "Optimiere Größe für Modell", + "useSize": "Maße übernehmen", + "remixImage": "Remix des Bilds erstellen", + "imageActions": "Weitere Bildaktionen", + "invoke": { + "noNodesInGraph": "Keine Knoten im Graphen", + "canvasIsTransforming": "Leinwand ist beschäftigt (wird transformiert)", + "canvasIsRasterizing": "Leinwand ist beschäftigt (wird gerastert)", + "canvasIsCompositing": "Leinwand ist beschäftigt (wird zusammengesetzt)", + "canvasIsFiltering": "Leinwand ist beschäftigt (wird gefiltert)", + "canvasIsSelectingObject": "Leinwand ist beschäftigt (wird Objekt ausgewählt)", + "noPrompts": "Keine Eingabeaufforderungen generiert", + "noModelSelected": "Kein Modell ausgewählt" + }, + "seed": "Seed", + "patchmatchDownScaleSize": "Herunterskalieren", + "seamlessXAxis": "Nahtlose X Achse", + "seamlessYAxis": "Nahtlose Y Achse", + "coherenceEdgeSize": "Kantengröße", + "infillColorValue": "Füllfarbe", + "controlNetControlMode": "Kontrollmodus", + "cancel": { + "cancel": "Abbrechen" + }, + "iterations": "Iterationen", + "guidance": "Führung", + "coherenceMode": "Modus", + "recallMetadata": "Metadaten abrufen", + "gaussianBlur": "Gaußsche Unschärfe", + "sendToUpscale": "An Hochskalieren senden", + "useCpuNoise": "CPU-Rauschen verwenden", + "sendToCanvas": "An Leinwand senden", + "disabledNoRasterContent": "Deaktiviert (kein Rasterinhalt)" + }, + "settings": { + "displayInProgress": "Zwischenbilder anzeigen", + "confirmOnDelete": "Bestätigen beim Löschen", + "resetWebUI": "Web-Oberfläche zurücksetzen", + "resetWebUIDesc1": "Das Zurücksetzen der Web-Oberfläche setzt nur den lokalen Cache des Browsers mit Ihren Bildern und gespeicherten Einstellungen zurück. Es werden keine Bilder von der Festplatte gelöscht.", + "resetWebUIDesc2": "Wenn die Bilder nicht in der Galerie angezeigt werden oder etwas anderes nicht funktioniert, versuchen Sie bitte, die Einstellungen zurückzusetzen, bevor Sie einen Fehler auf GitHub melden.", + "resetComplete": "Die Web-Oberfläche wurde zurückgesetzt.", + "models": "Modelle", + "clearIntermediatesDesc1": "Das Löschen der Zwischenbilder setzt Leinwand und ControlNet zurück.", + "generation": "Erzeugung", + "enableInformationalPopovers": "Info-Popouts anzeigen", + "showProgressInViewer": "Zwischenbilder im Viewer anzeigen", + "clearIntermediatesDesc3": "Ihre Bilder werden nicht gelöscht.", + "clearIntermediatesWithCount_one": "Lösche {{count}} Zwischenbilder", + "clearIntermediatesWithCount_other": "Lösche {{count}} Zwischenbilder", + "reloadingIn": "Neuladen in", + "intermediatesCleared_one": "{{count}} Zwischenbilder gelöscht", + "intermediatesCleared_other": "{{count}} Zwischenbilder gelöscht", + "enableInvisibleWatermark": "Unsichtbares Wasserzeichen aktivieren", + "general": "Allgemein", + "clearIntermediatesDisabled": "Warteschlange muss leer sein, um Zwischenbilder zu löschen", + "developer": "Entwickler", + "antialiasProgressImages": "Zwischenbilder mit Anti-Alias", + "beta": "Beta", + "ui": "Benutzeroberfläche", + "clearIntermediatesDesc2": "Zwischenbilder sind Nebenprodukte der Erstellung. Sie zu löschen macht Festplattenspeicher frei.", + "clearIntermediates": "Zwischenbilder löschen", + "intermediatesClearedFailed": "Problem beim Löschen der Zwischenbilder", + "enableNSFWChecker": "Auf unangemessene Inhalte prüfen" + }, + "toast": { + "uploadFailed": "Hochladen fehlgeschlagen", + "imageCopied": "Bild kopiert", + "parametersNotSet": "Parameter nicht zurückgerufen", + "addedToBoard": "Dem Board hinzugefügt", + "loadedWithWarnings": "Workflow mit Warnungen geladen", + "linkCopied": "Link kopiert", + "problemCopyingLayer": "Ebene kann nicht kopiert werden", + "problemSavingLayer": "Ebene kann nicht gespeichert werden", + "parameterSetDesc": "{{parameter}} zurückgerufen", + "imageUploaded": "Bild hochgeladen", + "problemCopyingImage": "Bild kann nicht kopiert werden", + "parameterNotSetDesc": "{{parameter}} kann nicht zurückgerufen werden", + "prunedQueue": "Warteschlange bereinigt", + "modelAddedSimple": "Modell zur Warteschlange hinzugefügt", + "parametersSet": "Parameter zurückgerufen", + "sentToUpscale": "An Vergrößerung gesendet", + "parameterNotSetDescWithMessage": "{{parameter}} kann nicht zurückgerufen werden: {{message}}", + "unableToLoadImageMetadata": "Bildmetadaten können nicht geladen werden", + "unableToLoadImage": "Bild kann nicht geladen werden", + "serverError": "Serverfehler", + "parameterNotSet": "Parameter nicht zurückgerufen", + "sessionRef": "Sitzung: {{sessionId}}", + "problemDownloadingImage": "Bild kann nicht heruntergeladen werden", + "parameters": "Parameter", + "parameterSet": "Parameter zurückgerufen", + "importFailed": "Import fehlgeschlagen", + "importSuccessful": "Import erfolgreich", + "somethingWentWrong": "Etwas ist schief gelaufen", + "workflowLoaded": "Arbeitsablauf geladen", + "workflowDeleted": "Arbeitsablauf gelöscht", + "errorCopied": "Fehler kopiert", + "layerCopiedToClipboard": "Ebene in die Zwischenablage kopiert", + "sentToCanvas": "An Leinwand gesendet", + "problemDeletingWorkflow": "Problem beim Löschen des Arbeitsablaufs", + "problemRetrievingWorkflow": "Problem beim Abrufen des Arbeitsablaufs", + "uploadFailedInvalidUploadDesc": "Müssen PNG-, JPEG- oder WEBP-Bilder sein.", + "pasteSuccess": "Eingefügt in {{destination}}", + "pasteFailed": "Einfügen fehlgeschlagen", + "unableToCopy": "Kopieren nicht möglich", + "unableToCopyDesc_theseSteps": "diese Schritte", + "noVisibleRasterLayers": "Keine sichtbaren Rasterebenen" + }, + "accessibility": { + "uploadImage": "Bild hochladen", + "previousImage": "Vorheriges Bild", + "reset": "Zurücksetzten", + "nextImage": "Nächstes Bild", + "menu": "Menü", + "invokeProgressBar": "Invoke Fortschrittsanzeige", + "mode": "Modus", + "resetUI": "$t(accessibility.reset) von UI", + "createIssue": "Ticket erstellen", + "about": "Über", + "submitSupportTicket": "Support-Ticket senden", + "toggleRightPanel": "Rechtes Bedienfeld umschalten (G)", + "toggleLeftPanel": "Linkes Bedienfeld umschalten (T)", + "uploadImages": "Bild(er) hochladen" + }, + "boards": { + "autoAddBoard": "Board automatisch erstellen", + "topMessage": "Diese Auswahl enthält Bilder, die in den folgenden Funktionen verwendet werden:", + "move": "Bewegen", + "menuItemAutoAdd": "Auto-Hinzufügen zu diesem Ordner", + "myBoard": "Meine Ordner", + "searchBoard": "Ordner durchsuchen...", + "noMatching": "Keine passenden Ordner", + "selectBoard": "Ordner wählen", + "cancel": "Abbrechen", + "addBoard": "Board hinzufügen", + "uncategorized": "Ohne Kategorie", + "downloadBoard": "Ordner runterladen", + "changeBoard": "Ordner wechseln", + "loading": "Laden...", + "clearSearch": "Suche leeren", + "bottomMessage": "Durch das Löschen von Bildern werden alle Funktionen zurückgesetzt, die diese Bilder derzeit verwenden.", + "deleteBoardOnly": "Nur Ordner löschen", + "deleteBoard": "Lösche Ordner", + "deleteBoardAndImages": "Lösche Ordner und Bilder", + "movingImagesToBoard_one": "Verschiebe {{count}} Bild in Ordner:", + "movingImagesToBoard_other": "Verschiebe {{count}} Bilder in Ordner:", + "selectedForAutoAdd": "Ausgewählt für Automatisches hinzufügen", + "imagesWithCount_one": "{{count}} Bild", + "imagesWithCount_other": "{{count}} Bilder", + "addPrivateBoard": "Privaten Ordner hinzufügen", + "addSharedBoard": "Geteilten Ordner hinzufügen", + "boards": "Ordner", + "unarchiveBoard": "Unarchive Ordner", + "private": "Private Ordner", + "shared": "Geteilte Ordner", + "archiveBoard": "Ordner archivieren", + "archived": "Archiviert", + "noBoards": "Kein {{boardType}} Ordner", + "deletedPrivateBoardsCannotbeRestored": "Gelöschte Pinnwände und Bilder können nicht wiederhergestellt werden. Wenn Sie „Nur Pinnwand löschen“ auswählen, werden die Bilder in einen privaten, nicht kategorisierten Zustand verschoben, der nur dem Ersteller des Bildes zugänglich ist.", + "assetsWithCount_one": "{{count}} in der Sammlung", + "assetsWithCount_other": "{{count}} in der Sammlung", + "deletedBoardsCannotbeRestored": "Gelöschte Boards und Bilder können nicht wiederhergestellt werden. Durch Auswahl von „Nur Board löschen“ werden die Bilder in den nicht kategorisierten Zustand verschoben.", + "updateBoardError": "Fehler beim Aktualisieren des Ordners", + "uncategorizedImages": "Nicht kategorisierte Bilder", + "deleteAllUncategorizedImages": "Alle nicht kategorisierten Bilder löschen", + "pause": "Pause", + "resume": "Weiter", + "restartFailed": "Neustart fehlgeschlagen", + "restartFile": "Datei erneut starten", + "restartRequired": "Neustart erforderlich", + "resumeRefused": "Der Server hat die Fortsetzung des Vorgangs abgelehnt. Ein Neustart ist erforderlich.", + "deletedImagesCannotBeRestored": "Gelöschte Bilder können nicht wiederhergestellt werden.", + "hideBoards": "Ordner verstecken", + "locateInGalery": "In der Galerie finden", + "viewBoards": "Ordner anzeigen", + "setBoardVisibility": "Sichtbarkeit des Ordner", + "setVisibilityPrivate": "Privat einstellen" + }, + "queue": { + "status": "Status", + "cancelTooltip": "Aufgabe abbrechen", + "queueEmpty": "Warteschlange leer", + "in_progress": "In Arbeit", + "queueFront": "Am Anfang der Warteschlange einreihen", + "completed": "Fertig", + "queueBack": "In die Warteschlange", + "clearFailed": "Probleme beim leeren der Warteschlange", + "clearSucceeded": "Warteschlange geleert", + "pause": "Pause", + "cancelSucceeded": "Auftrag abgebrochen", + "queue": "Warteschlange", + "batch": "Stapel", + "pending": "Ausstehend", + "clear": "Leeren", + "prune": "Leeren", + "total": "Gesamt", + "canceled": "Abgebrochen", + "clearTooltip": "Abbrechen und alle Aufträge leeren", + "current": "Aktuell", + "failed": "Fehler", + "cancelItem": "Abbruch Auftrag", + "next": "Nächste", + "cancel": "Abbruch", + "session": "Sitzung", + "resume": "Wieder aufnehmen", + "item": "Auftrag", + "notReady": "Warteschlange noch nicht bereit", + "clearQueueAlertDialog": "\"Die Warteschlange leeren\" stoppt den aktuellen Prozess und leert die Warteschlange komplett.", + "completedIn": "Fertig in", + "cancelBatchSucceeded": "Stapel abgebrochen", + "cancelBatch": "Stapel stoppen", + "enqueueing": "Stapel in der Warteschlange", + "cancelBatchFailed": "Problem beim Abbruch vom Stapel", + "clearQueueAlertDialog2": "Warteschlange wirklich leeren?", + "pruneSucceeded": "{{item_count}} abgeschlossene Elemente aus der Warteschlange entfernt", + "pauseSucceeded": "Prozess angehalten", + "cancelFailed": "Problem beim Abbrechen", + "pauseFailed": "Problem beim Anhalten des Prozesses", + "front": "Vorne", + "pruneTooltip": "Bereinigen Sie {{item_count}} abgeschlossene Aufträge", + "resumeFailed": "Problem beim Fortsetzen des Prozesses", + "pruneFailed": "Problem beim leeren der Warteschlange", + "pauseTooltip": "Prozess anhalten", + "back": "Ende", + "resumeSucceeded": "Prozess wird fortgesetzt", + "resumeTooltip": "Prozess wieder aufnehmen", + "time": "Zeit", + "batchQueuedDesc_one": "{{count}} Eintrag an {{direction}} der Wartschlange hinzugefügt", + "batchQueuedDesc_other": "{{count}} Einträge an {{direction}} der Wartschlange hinzugefügt", + "openQueue": "Warteschlange öffnen", + "batchFailedToQueue": "Fehler beim Einreihen in die Stapelverarbeitung", + "batchQueued": "Stapelverarbeitung eingereiht", + "graphQueued": "Graph eingereiht", + "graphFailedToQueue": "Fehler beim Einreihen des Graphen", + "generations_one": "Generation", + "generations_other": "Generationen", + "iterations_one": "Iteration", + "iterations_other": "Iterationen", + "gallery": "Galerie", + "generation": "Erstellung", + "workflows": "Arbeitsabläufe", + "other": "Sonstige", + "origin": "Ursprung", + "destination": "Ziel", + "upscaling": "Hochskalierung", + "canvas": "Leinwand", + "prompts_one": "Prompt", + "prompts_other": "Prompts", + "batchSize": "Stapelgröße", + "confirm": "Bestätigen" + }, + "metadata": { + "negativePrompt": "Negativ Beschreibung", + "metadata": "Meta-Daten", + "strength": "Bild zu Bild Stärke", + "imageDetails": "Bild Details", + "model": "Modell", + "noImageDetails": "Keine Bild Details gefunden", + "cfgScale": "CFG-Skala", + "height": "Höhe", + "noMetaData": "Keine Meta-Daten gefunden", + "width": "Breite", + "createdBy": "Erstellt von", + "steps": "Schritte", + "positivePrompt": "Positiver Prompt", + "generationMode": "Generierungsmodus", + "Threshold": "Rauschen-Schwelle", + "seed": "Seed", + "vae": "VAE", + "workflow": "Workflow", + "scheduler": "Planer", + "noRecallParameters": "Es wurden keine Parameter zum Abrufen gefunden", + "recallParameters": "Parameter wiederherstellen", + "cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)", + "allPrompts": "Alle Prompts", + "imageDimensions": "Bilder Auslösungen", + "parameterSet": "Parameter {{parameter}} setzen", + "canvasV2Metadata": "Leinwand", + "guidance": "Führung", + "seamlessXAxis": "Nahtlose X Achse", + "seamlessYAxis": "Nahtlose Y Achse" + }, + "popovers": { + "noiseUseCPU": { + "heading": "Nutze CPU-Rauschen", + "paragraphs": [ + "Entscheidet, ob auf der CPU oder GPU Rauschen erzeugt wird.", + "Mit aktiviertem CPU-Rauschen wird ein bestimmter Seedwert das gleiche Bild auf jeder Maschine erzeugen.", + "CPU-Rauschen einzuschalten beeinflusst nicht die Systemleistung." + ] + }, + "paramModel": { + "heading": "Modell", + "paragraphs": [ + "Modell für die Entrauschungsschritte." + ] + }, + "paramIterations": { + "heading": "Iterationen", + "paragraphs": [ + "Die Anzahl der Bilder, die erzeugt werden sollen.", + "Wenn \"Dynamische Prompts\" aktiviert ist, wird jeder einzelne Prompt so oft generiert." + ] + }, + "paramCFGScale": { + "heading": "CFG-Skala", + "paragraphs": [ + "Bestimmt, wie viel Ihr Prompt den Erzeugungsprozess beeinflusst." + ] + }, + "paramSteps": { + "heading": "Schritte", + "paragraphs": [ + "Anzahl der Schritte, die bei jeder Generierung durchgeführt werden.", + "Höhere Schrittzahlen werden in der Regel bessere Bilder ergeben, aber mehr Zeit benötigen." + ] + }, + "lora": { + "heading": "LoRA Gewichte", + "paragraphs": [ + "Höhere LoRA-Wichtungen führen zu größeren Auswirkungen auf das endgültige Bild." + ] + }, + "infillMethod": { + "heading": "Füllmethode", + "paragraphs": [ + "Infill-Methode für den ausgewählten Bereich." + ] + }, + "paramVAE": { + "heading": "VAE", + "paragraphs": [ + "Verwendetes Modell, um den KI-Ausgang in das endgültige Bild zu übersetzen." + ] + }, + "paramRatio": { + "heading": "Seitenverhältnis", + "paragraphs": [ + "Das Seitenverhältnis des erzeugten Bildes.", + "Für SD1.5-Modelle wird eine Bildgröße von 512x512 Pixel empfohlen, für SDXL-Modelle sind es 1024x1024 Pixel." + ] + }, + "paramDenoisingStrength": { + "paragraphs": [ + "Wie viel Rauschen dem Eingabebild hinzugefügt wird.", + "0 wird zu einem identischen Bild führen, während 1 zu einem völlig neuen Bild führt." + ], + "heading": "Stärke der Entrauschung" + }, + "paramVAEPrecision": { + "heading": "VAE-Präzision", + "paragraphs": [ + "Die bei der VAE-Kodierung und Dekodierung verwendete Präzision. FP16/Halbpräzision ist effizienter, aber auf Kosten kleiner Bildvariationen." + ] + }, + "paramCFGRescaleMultiplier": { + "heading": "CFG Rescale Multiplikator", + "paragraphs": [ + "Rescale-Multiplikator für die CFG-Lenkung, der für Modelle verwendet wird, die mit dem zero-terminal SNR (ztsnr) trainiert wurden. Empfohlener Wert: 0,7." + ] + }, + "scaleBeforeProcessing": { + "paragraphs": [ + "Skaliert den ausgewählten Bereich auf die Größe, die für das Modell am besten geeignet ist." + ], + "heading": "Skalieren vor der Verarbeitung" + }, + "paramSeed": { + "paragraphs": [ + "Kontrolliert das für die Erzeugung verwendete Startrauschen.", + "Deaktivieren Sie “Random Seed”, um identische Ergebnisse mit den gleichen Generierungseinstellungen zu erzeugen." + ], + "heading": "Seed" + }, + "dynamicPromptsMaxPrompts": { + "paragraphs": [ + "Beschränkt die Anzahl der Prompts, die von \"Dynamic Prompts\" generiert werden können." + ], + "heading": "Maximale Prompts" + }, + "dynamicPromptsSeedBehaviour": { + "paragraphs": [ + "Bestimmt, wie der Seed-Wert beim Erzeugen von Prompts verwendet wird.", + "Verwenden Sie dies, um schnelle Variationen eines einzigen Seeds zu erkunden.", + "Wenn Sie z. B. 5 Prompts haben, wird jedes Bild den selben Seed-Wert verwenden.", + "\"Per Bild\" wird einen einzigartigen Seed-Wert für jedes Bild verwenden. Dies bietet mehr Variationen." + ], + "heading": "Seed-Verhalten" + }, + "dynamicPrompts": { + "paragraphs": [ + "\"Dynamische Prompts\" übersetzt einen Prompt in mehrere.", + "Die Ausgangs-Syntax ist \"ein {roter|grüner|blauer} ball\". Das generiert 3 Prompts: \"ein roter ball\", \"ein grüner ball\" und \"ein blauer ball\".", + "Sie können die Syntax so oft verwenden, wie Sie in einem einzigen Prompt möchten, aber stellen Sie sicher, dass die Anzahl der Prompts zur Einstellung von \"Max Prompts\" passt." + ], + "heading": "Dynamische Prompts" + }, + "controlNetWeight": { + "paragraphs": [ + "Wie stark wird das ControlNet das generierte Bild beeinflussen wird." + ], + "heading": "Einfluss" + }, + "paramScheduler": { + "paragraphs": [ + "Verwendeter Planer währende des Generierungsprozesses.", + "Jeder Planer definiert, wie einem Bild iterativ Rauschen hinzugefügt wird, oder wie ein Sample basierend auf der Ausgabe eines Modells aktualisiert wird." + ], + "heading": "Planer" + }, + "imageFit": { + "paragraphs": [ + "Reduziert das Ausgangsbild auf die Breite und Höhe des Ausgangsbildes. Empfohlen zu aktivieren." + ] + }, + "structure": { + "paragraphs": [ + "Die Struktur steuert, wie genau sich das Ausgabebild an das Layout des Originals hält. Eine niedrige Struktur erlaubt größere Änderungen, während eine hohe Struktur die ursprüngliche Komposition und das Layout strikter beibehält." + ] + }, + "creativity": { + "paragraphs": [ + "Die Kreativität bestimmt den Grad der Freiheit, die dem Modell beim Hinzufügen von Details gewährt wird. Eine niedrige Kreativität hält sich eng an das Originalbild, während eine hohe Kreativität mehr Veränderungen zulässt. Bei der Verwendung eines Prompts erhöht eine hohe Kreativität den Einfluss des Prompts." + ] + }, + "scale": { + "paragraphs": [ + "Die Skalierung steuert die Größe des Ausgabebildes und basiert auf einem Vielfachen der Auflösung des Originalbildes. So würde z. B. eine 2-fache Hochskalierung eines 1024x1024px Bildes eine 2048x2048px große Ausgabe erzeugen." + ] + }, + "ipAdapterMethod": { + "heading": "Methode" + }, + "refinerScheduler": { + "heading": "Planer", + "paragraphs": [ + "Planer, der während der Veredelungsphase des Generierungsprozesses verwendet wird.", + "Ähnlich wie der Generierungsplaner." + ] + }, + "compositingCoherenceMode": { + "paragraphs": [ + "Verwendete Methode zur Erstellung eines kohärenten Bildes mit dem neu generierten maskierten Bereich." + ], + "heading": "Modus" + }, + "compositingCoherencePass": { + "heading": "Kohärenzdurchlauf" + }, + "controlNet": { + "heading": "ControlNet" + }, + "compositingMaskAdjustments": { + "paragraphs": [ + "Die Maske anpassen." + ], + "heading": "Maskenanpassungen" + }, + "compositingMaskBlur": { + "paragraphs": [ + "Der Unschärferadius der Maske." + ], + "heading": "Maskenunschärfe" + }, + "compositingBlurMethod": { + "paragraphs": [ + "Die auf den maskierten Bereich angewendete Unschärfemethode." + ], + "heading": "Unschärfemethode" + }, + "controlNetResizeMode": { + "heading": "Größenänderungsmodus" + }, + "paramWidth": { + "heading": "Breite", + "paragraphs": [ + "Breite des generierten Bildes. Muss ein Vielfaches von 8 sein." + ] + }, + "controlNetControlMode": { + "heading": "Kontrollmodus" + }, + "controlNetProcessor": { + "heading": "Prozessor" + }, + "patchmatchDownScaleSize": { + "heading": "Herunterskalieren" + }, + "paramHeight": { + "heading": "Höhe", + "paragraphs": [ + "Höhe des generierten Bildes. Muss ein Vielfaches von 8 sein." + ] + }, + "paramUpscaleMethod": { + "heading": "Vergrößerungsmethode", + "paragraphs": [ + "Methode zum Hochskalieren des Bildes für High Resolution Fix." + ] + }, + "paramHrf": { + "heading": "High Resolution Fix aktivieren" + }, + "seamlessTilingYAxis": { + "heading": "Nahtlose Kachelung Y Achse", + "paragraphs": [ + "Nahtloses Kacheln eines Bildes entlang der vertikalen Achse." + ] + }, + "seamlessTilingXAxis": { + "paragraphs": [ + "Nahtloses Kacheln eines Bildes entlang der horizontalen Achse." + ], + "heading": "Nahtlose Kachelung X Achse" + }, + "compositingCoherenceEdgeSize": { + "paragraphs": [ + "Die Kantengröße des Kohärenzdurchlaufs." + ], + "heading": "Kantengröße" + }, + "rasterLayer": { + "heading": "Rasterebene" + } + }, + "invocationCache": { + "disable": "Deaktivieren", + "misses": "Cache nicht genutzt", + "hits": "Cache Treffer", + "enable": "Aktivieren", + "clear": "Leeren", + "maxCacheSize": "Maximale Cache Größe", + "cacheSize": "Cache Größe", + "useCache": "Benutze Cache", + "enableFailed": "Problem beim Aktivieren des Zwischenspeichers", + "disableFailed": "Problem bei Deaktivierung des Cache", + "enableSucceeded": "Zwischenspeicher aktiviert", + "disableSucceeded": "Invocation-Cache deaktiviert", + "clearSucceeded": "Zwischenspeicher gelöscht", + "invocationCache": "Zwischenspeicher", + "clearFailed": "Problem beim Löschen des Zwischenspeichers" + }, + "nodes": { + "addNode": "Knoten hinzufügen", + "colorCodeEdgesHelp": "Farbkodieren Sie Kanten entsprechend ihren verbundenen Feldern", + "animatedEdges": "Animierte Kanten", + "animatedEdgesHelp": "Animieren Sie ausgewählte Kanten und Kanten, die mit ausgewählten Knoten verbunden sind", + "cannotDuplicateConnection": "Es können keine doppelten Verbindungen erstellt werden", + "boolean": "Boolesche Werte", + "currentImage": "Aktuelles Bild", + "collection": "Sammlung", + "cannotConnectInputToInput": "Eingang kann nicht mit Eingang verbunden werden", + "cannotConnectOutputToOutput": "Ausgang kann nicht mit Ausgang verbunden werden", + "cannotConnectToSelf": "Es kann keine Verbindung zu sich selbst hergestellt werden", + "colorCodeEdges": "Farbkodierte Kanten", + "addNodeToolTip": "Knoten hinzufügen (Umschalt+A, Leertaste)", + "collectionFieldType": "{{name}} (Sammlung)", + "connectionWouldCreateCycle": "Verbindung würde einen Kreislauf/cycle schaffen", + "inputMayOnlyHaveOneConnection": "Eingang darf nur eine Verbindung haben", + "integer": "Ganze Zahl", + "currentImageDescription": "Zeigt das aktuelle Bild im Node-Editor an", + "ipAdapter": "IP-Adapter", + "hideMinimapnodes": "Miniatur-Kartenansicht ausblenden", + "newWorkflowDesc2": "Ihr aktueller Arbeitsablauf hat ungespeicherte Änderungen.", + "problemSettingTitle": "Problem beim Einstellen des Titels", + "reloadNodeTemplates": "Knoten-Vorlagen neu laden", + "newWorkflow": "Neuer Arbeitsablauf / Workflow", + "newWorkflowDesc": "Einen neuen Arbeitsablauf erstellen?", + "clearWorkflow": "Workflow löschen", + "clearWorkflowDesc": "Diesen Arbeitsablauf löschen und neu starten?", + "noConnectionInProgress": "Es besteht keine Verbindung", + "notes": "Anmerkungen", + "nodeVersion": "Knoten Version", + "node": "Knoten", + "nodeSearch": "Knoten suchen", + "nodeOutputs": "Knoten-Ausgänge", + "nodeTemplate": "Knoten-Vorlage", + "nodeType": "Knotentyp", + "noNodeSelected": "Kein Knoten gewählt", + "nodeOpacity": "Knoten-Deckkraft", + "noOutputRecorded": "Keine Ausgänge aufgezeichnet", + "notesDescription": "Anmerkungen zum Arbeitsablauf hinzufügen", + "clearWorkflowDesc2": "Ihr aktueller Arbeitsablauf hat ungespeicherte Änderungen.", + "scheduler": "Planer", + "showMinimapnodes": "MiniMap anzeigen", + "executionStateCompleted": "Erledigt", + "downloadWorkflow": "Workflow JSON herunterladen", + "executionStateInProgress": "In Bearbeitung", + "snapToGridHelp": "Knoten am Gitternetz einrasten bei Bewegung", + "missingTemplate": "Ungültiger Knoten: Knoten {{node}} vom Typ {{type}} fehlt Vorlage (nicht installiert?)", + "string": "Zeichenfolge", + "fieldTypesMustMatch": "Feldtypen müssen übereinstimmen", + "fitViewportNodes": "An Ansichtsgröße anpassen", + "loadingNodes": "Lade Nodes...", + "fullyContainNodesHelp": "Nodes müssen vollständig innerhalb der Auswahlbox sein, um ausgewählt werden zu können", + "noWorkflow": "Kein Workflow", + "executionStateError": "Fehler", + "nodePack": "Knoten-Pack", + "loadWorkflow": "Lade Workflow", + "snapToGrid": "Am Gitternetz einrasten", + "updateNode": "Knoten updaten", + "edge": "Rand / Kante", + "sourceNodeDoesNotExist": "Ungültiger Rand: Quell- / Ausgabe-Knoten {{node}} existiert nicht", + "updateAllNodes": "Update Knoten", + "allNodesUpdated": "Alle Knoten aktualisiert", + "updateApp": "Update App", + "unknownNodeType": "Unbekannter Knotentyp", + "float": "Kommazahlen", + "enum": "Aufzählung", + "fullyContainNodes": "Vollständig ausgewählte Nodes auswählen", + "editMode": "Im Workflow-Editor bearbeiten", + "resetToDefaultValue": "Auf Standardwert zurücksetzen", + "singleFieldType": "{{name}} (Einzeln)", + "collectionOrScalarFieldType": "{{name}} (Einzeln oder Sammlung)", + "missingFieldTemplate": "Fehlende Feldvorlage", + "missingNode": "Fehlender Aufrufknoten", + "missingInvocationTemplate": "Fehlende Aufrufvorlage", + "edit": "Bearbeiten", + "workflowAuthor": "Autor", + "graph": "Graph", + "workflowDescription": "Kurze Beschreibung", + "workflow": "Arbeitsablauf", + "noGraph": "Kein Graph", + "version": "Version", + "zoomInNodes": "Hineinzoomen", + "zoomOutNodes": "Herauszoomen", + "workflowName": "Name", + "unknownNode": "Unbekannter Knoten", + "workflowContact": "Kontaktdaten", + "workflowNotes": "Notizen", + "workflowTags": "Tags", + "workflowVersion": "Version", + "saveToGallery": "In Galerie speichern", + "noWorkflows": "Keine Arbeitsabläufe", + "noMatchingWorkflows": "Keine passenden Arbeitsabläufe", + "unknownErrorValidatingWorkflow": "Unbekannter Fehler beim Validieren des Arbeitsablaufes", + "inputFieldTypeParseError": "Typ des Eingabefelds {{node}}.{{field}} kann nicht analysiert werden ({{message}})", + "workflowSettings": "Arbeitsablauf Editor Einstellungen", + "viewMode": "In linearen Ansicht verwenden", + "unableToValidateWorkflow": "Arbeitsablauf kann nicht validiert werden", + "outputFieldTypeParseError": "Typ des Ausgabefelds {{node}}.{{field}} kann nicht analysiert werden ({{message}})", + "unableToGetWorkflowVersion": "Version des Arbeitsablaufschemas kann nicht bestimmt werden", + "unknownFieldType": "$t(nodes.unknownField) Typ: {{type}}", + "unknownField": "Unbekanntes Feld", + "unableToUpdateNodes_one": "{{count}} Knoten kann nicht aktualisiert werden", + "unableToUpdateNodes_other": "{{count}} Knoten können nicht aktualisiert werden", + "uniformRandomDistribution": "Uniforme Zufallsverteilung", + "linearDistribution": "Lineare Verteilung", + "generatorNRandomValues_one": "{{count}} Zufallswert", + "generatorNRandomValues_other": "{{count}} Zufallswerte", + "arithmeticSequence": "Arithmetische Folge", + "noBatchGroup": "keine Gruppe", + "generatorNoValues": "leer", + "generatorLoadFromFile": "Aus Datei laden", + "showEdgeLabels": "Kantenbeschriftungen anzeigen", + "downloadWorkflowError": "Fehler beim Herunterladen des Arbeitsablaufs", + "nodeName": "Knotenname", + "description": "Beschreibung", + "loadWorkflowDesc": "Arbeitsablauf laden?", + "loadWorkflowDesc2": "Ihr aktueller Arbeitsablauf enthält nicht gespeicherte Änderungen.", + "missingSourceOrTargetHandle": "Fehlender Quell- oder Zielgriff", + "missingSourceOrTargetNode": "Fehlender Quell- oder Zielknoten", + "showEdgeLabelsHelp": "Beschriftungen an Kanten anzeigen, um die verknüpften Knoten zu kennzeichnen" + }, + "hrf": { + "metadata": { + "strength": "Auflösungs-Fix Stärke", + "enabled": "Auflösungs-Fix aktiviert", + "method": "Auflösungs-Fix Methode" + }, + "hrf": "Hohe-Auflösung-Fix" + }, + "models": { + "noMatchingModels": "Keine passenden Modelle", + "loading": "lade", + "noModelsAvailable": "Keine Modelle verfügbar", + "selectModel": "Wählen ein Modell aus", + "noRefinerModelsInstalled": "Keine SDXL Refiner-Modelle installiert", + "addLora": "LoRA hinzufügen", + "defaultVAE": "Standard VAE", + "lora": "LoRA", + "concepts": "Konzepte" + }, + "accordions": { + "generation": { + "title": "Erstellung" + }, + "image": { + "title": "Bild" + }, + "advanced": { + "title": "Erweitert", + "options": "$t(accordions.advanced.title) Optionen" + }, + "control": { + "title": "Kontrolle" + }, + "compositing": { + "coherenceTab": "Kohärenzpass", + "infillTab": "Infill", + "title": "Compositing" + } + }, + "workflows": { + "workflows": "Arbeitsabläufe", + "workflowName": "Arbeitsablauf-Name", + "saveWorkflowAs": "Arbeitsablauf speichern als", + "newWorkflowCreated": "Neuer Arbeitsablauf erstellt", + "problemSavingWorkflow": "Problem beim Speichern des Arbeitsablaufs", + "downloadWorkflow": "Speichern als", + "savingWorkflow": "Speichere Arbeitsablauf...", + "saveWorkflow": "Arbeitsablauf speichern", + "noWorkflows": "Keine Arbeitsabläufe", + "workflowLibrary": "Bibliothek", + "unnamedWorkflow": "Unbenannter Arbeitsablauf", + "workflowEditorMenu": "Arbeitsablauf-Editor Menü", + "deleteWorkflow": "Arbeitsablauf löschen", + "workflowSaved": "Arbeitsablauf gespeichert", + "uploadWorkflow": "Aus Datei laden", + "saveWorkflowToProject": "Arbeitsablauf in Projekt speichern", + "workflowCleared": "Arbeitsablauf gelöscht", + "loading": "Lade Arbeitsabläufe", + "name": "Name", + "ascending": "Aufsteigend", + "opened": "Geöffnet", + "loadWorkflow": "Arbeitsablauf $t(common.load)", + "updated": "Aktualisiert", + "created": "Erstellt", + "descending": "Absteigend", + "edit": "Bearbeiten", + "loadFromGraph": "Arbeitsablauf aus dem Graph laden", + "delete": "Löschen", + "copyShareLinkForWorkflow": "Teilen-Link für Arbeitsablauf kopieren", + "autoLayout": "Auto Layout", + "copyShareLink": "Teilen-Link kopieren", + "download": "Herunterladen", + "convertGraph": "Graph konvertieren", + "yourWorkflows": "Ihre Arbeitsabläufe", + "recentlyOpened": "Kürzlich geöffnet" + }, + "sdxl": { + "scheduler": "Planer", + "steps": "Schritte" + }, + "dynamicPrompts": { + "showDynamicPrompts": "Dynamische Prompts anzeigen" + }, + "prompt": { + "noMatchingTriggers": "Keine passenden Trigger", + "addPromptTrigger": "Prompt-Trigger hinzufügen", + "compatibleEmbeddings": "Kompatible Einbettungen", + "replace": "Ersetzen", + "discard": "Verwerfen", + "generateFromImage": "Prompt aus Bild generieren", + "expandCurrentPrompt": "Aktuelle Prompt erweitern", + "uploadImageForPromptGeneration": "Bild zur Prompt-Generierung hochladen", + "expandingPrompt": "Prompt wird erweitert..." + }, + "ui": { + "tabs": { + "queue": "Warteschlange", + "gallery": "Galerie", + "models": "Modelle", + "upscaling": "Hochskalierung", + "workflows": "Arbeitsabläufe", + "canvas": "Leinwand" + } + }, + "system": { + "logNamespaces": { + "logNamespaces": "Namespaces loggen", + "models": "Modelle", + "gallery": "Galerie", + "events": "Ereignisse", + "queue": "Warteschlange", + "system": "System", + "workflows": "Arbeitsabläufe", + "generation": "Erstellung", + "metadata": "Metadaten", + "config": "Konfiguration", + "canvas": "Leinwand" + }, + "logLevel": { + "fatal": "Fatal", + "trace": "Trace", + "logLevel": "Protokollierungsstufe", + "error": "Fehler", + "info": "Infos", + "warn": "Warnung", + "debug": "Fehlerdiagnose" + }, + "enableLogging": "Protokollierung aktivieren" + }, + "whatsNew": { + "whatsNewInInvoke": "Was gibt's Neues" + }, + "stylePresets": { + "name": "Name", + "acceptedColumnsKeys": "Akzeptierte Spalten/Schlüssel:", + "noTemplates": "Keine Vorlagen", + "promptTemplatesDesc2": "Verwenden Sie die Platzhalterzeichenfolge
{{placeholder}}
, um anzugeben, wo Ihre Eingabeaufforderung in die Vorlage aufgenommen werden soll.", + "noMatchingTemplates": "Keine passenden Vorlagen", + "myTemplates": "Meine Vorlagen", + "toggleViewMode": "Ansicht umschalten", + "viewModeTooltip": "So sieht Ihr Prompt mit der aktuell ausgewählten Vorlage aus. Um Ihren Prompt zu bearbeiten, klicken Sie irgendwo in das Textfeld.", + "templateDeleted": "Promptvorlage gelöscht", + "unableToDeleteTemplate": "Promptvorlage kann nicht gelöscht werden", + "insertPlaceholder": "Platzhalter einfügen", + "type": "Typ", + "uploadImage": "Bild hochladen", + "updatePromptTemplate": "Promptvorlage aktualisieren", + "exportFailed": "CSV kann nicht generiert und heruntergeladen werden", + "viewList": "Vorlagenliste anzeigen", + "useForTemplate": "Für Promptvorlage nutzen", + "shared": "Geteilt", + "private": "Privat", + "promptTemplatesDesc1": "Promptvorlagen fügen den Prompts, die Sie in das Prompt-Feld schreiben, Text hinzu.", + "negativePrompt": "Negativ-Prompt", + "positivePromptColumn": "'prompt' oder 'positive_prompt'", + "promptTemplatesDesc3": "Wenn Sie den Platzhalter weglassen, wird die Vorlage an das Ende Ihres Prompts angehängt.", + "sharedTemplates": "Geteilte Vorlagen", + "importTemplates": "Promptvorlagen importieren (CSV/JSON)", + "flatten": "Ausgewählte Vorlage in aktuelle Eingabeaufforderung einblenden", + "searchByName": "Nach Name suchen", + "promptTemplateCleared": "Promptvorlage gelöscht", + "preview": "Vorschau", + "positivePrompt": "Positiv-Prompt", + "active": "Aktiv", + "deleteTemplate2": "Sind Sie sicher, dass Sie diese Vorlage löschen möchten? Dies kann nicht rückgängig gemacht werden.", + "deleteTemplate": "Vorlage löschen", + "copyTemplate": "Vorlage kopieren", + "editTemplate": "Vorlage bearbeiten", + "deleteImage": "Bild löschen", + "defaultTemplates": "Standardvorlagen", + "nameColumn": "'name'", + "exportDownloaded": "Export heruntergeladen" + }, + "newUserExperience": { + "gettingStartedSeries": "Wünschen Sie weitere Anleitungen? In unserer Einführungsserie finden Sie Tipps, wie Sie das Potenzial von Invoke Studio voll ausschöpfen können.", + "toGetStarted": "Um zu beginnen, geben Sie einen Prompt in das Feld ein und klicken Sie auf Invoke, um Ihr erstes Bild zu erzeugen. Sie können Ihre Bilder direkt in der Galerie speichern oder sie auf der Leinwand bearbeiten." + }, + "controlLayers": { + "pullBboxIntoLayerOk": "Bbox in die Ebene gezogen", + "saveBboxToGallery": "Bbox in Galerie speichern", + "tool": { + "bbox": "Bbox", + "brush": "Pinsel", + "eraser": "Radiergummi", + "colorPicker": "Farbwähler", + "view": "Ansicht", + "rectangle": "Rechteck", + "move": "Verschieben" + }, + "transform": { + "fitToBbox": "An Bbox anpassen", + "reset": "Zurücksetzen", + "apply": "Anwenden", + "cancel": "Abbrechen" + }, + "pullBboxIntoLayerError": "Problem, Bbox in die Ebene zu ziehen", + "pullBboxIntoLayer": "Bbox in Ebene ziehen", + "HUD": { + "bbox": "Bbox", + "scaledBbox": "Skalierte Bbox", + "entityStatus": { + "isHidden": "{{title}} ist ausgeblendet", + "isDisabled": "{{title}} ist deaktiviert", + "isLocked": "{{title}} ist gesperrt", + "isEmpty": "{{title}} ist leer" + } + }, + "fitBboxToLayers": "Bbox an Ebenen anpassen", + "pullBboxIntoReferenceImage": "Bbox ins Referenzbild ziehen", + "pullBboxIntoReferenceImageOk": "Bbox in Referenzbild gezogen", + "pullBboxIntoReferenceImageError": "Problem, Bbox ins Referenzbild zu ziehen", + "bboxOverlay": "Bbox Overlay anzeigen", + "clipToBbox": "Pinselstriche auf Bbox beschränken", + "canvasContextMenu": { + "saveBboxToGallery": "Bbox in Galerie speichern", + "bboxGroup": "Aus Bbox erstellen", + "canvasGroup": "Leinwand", + "newGlobalReferenceImage": "Neues globales Referenzbild", + "newRegionalReferenceImage": "Neues regionales Referenzbild", + "newControlLayer": "Neue Kontroll-Ebene", + "newRasterLayer": "Neue Rasterebene" + }, + "rectangle": "Rechteck", + "saveCanvasToGallery": "Leinwand in Galerie speichern", + "newRasterLayerError": "Problem beim Erstellen einer Rasterebene", + "saveLayerToAssets": "Ebene in Galerie speichern", + "deleteReferenceImage": "Referenzbild löschen", + "referenceImage": "Referenzbild", + "opacity": "Opazität", + "removeBookmark": "Lesezeichen entfernen", + "rasterLayer": "Rasterebene", + "deleteSelected": "Ausgewählte löschen", + "newRegionalReferenceImageError": "Problem beim Erstellen eines regionalen Referenzbilds", + "newControlLayerOk": "Kontroll-Ebene erstellt", + "newControlLayerError": "Problem beim Erstellen einer Kontroll-Ebene", + "newRasterLayerOk": "Rasterebene erstellt", + "moveToFront": "Nach vorne bringen", + "copyToClipboard": "In die Zwischenablage kopieren", + "clearCaches": "Cache leeren", + "controlLayer": "Kontroll-Ebene", + "transparency": "Transparenz", + "canvas": "Leinwand", + "global": "Global", + "regional": "Regional", + "newGlobalReferenceImageOk": "Globales Referenzbild erstellt", + "savedToGalleryError": "Fehler beim Speichern in der Galerie", + "savedToGalleryOk": "In Galerie gespeichert", + "newGlobalReferenceImageError": "Problem beim Erstellen eines globalen Referenzbilds", + "newRegionalReferenceImageOk": "Regionales Referenzbild erstellt", + "duplicate": "Duplizieren", + "regionalReferenceImage": "Regionales Referenzbild", + "globalReferenceImage": "Globales Referenzbild", + "regionIsEmpty": "Ausgewählte Region is leer", + "mergeVisible": "Sichtbare vereinen", + "mergeVisibleOk": "Sichtbare Ebenen vereinen", + "mergeVisibleError": "Fehler beim Vereinen sichtbarer Ebenen", + "clearHistory": "Verlauf leeren", + "addLayer": "Ebene hinzufügen", + "width": "Breite", + "weight": "Gewichtung", + "addReferenceImage": "$t(controlLayers.referenceImage) hinzufügen", + "addInpaintMask": "$t(controlLayers.inpaintMask) hinzufügen", + "regionalGuidance": "Regionale Führung", + "addPositivePrompt": "$t(controlLayers.prompt) hinzufügen", + "locked": "Gesperrt", + "showHUD": "HUD anzeigen", + "addNegativePrompt": "$t(controlLayers.negativePrompt) hinzufügen", + "addRasterLayer": "$t(controlLayers.rasterLayer) hinzufügen", + "addRegionalGuidance": "$t(controlLayers.regionalGuidance) hinzufügen", + "addControlLayer": "$t(controlLayers.controlLayer) hinzufügen", + "replaceLayer": "Ebene ersetzen", + "unlocked": "Entsperrt", + "showProgressOnCanvas": "Fortschritt auf Leinwand anzeigen", + "controlMode": { + "balanced": "Ausgewogen" + }, + "stagingArea": { + "accept": "Annehmen", + "next": "Nächste", + "discardAll": "Alle verwerfen", + "discard": "Verwerfen", + "previous": "Vorherige" + }, + "settings": { + "snapToGrid": { + "on": "Ein", + "off": "Aus", + "label": "Am Raster ausrichten" + } + }, + "layer_one": "Ebene", + "layer_other": "Ebenen", + "fill": { + "fillStyle": "Füllstil", + "diagonal": "Diagonal", + "vertical": "Vertikal", + "fillColor": "Füllfarbe", + "grid": "Raster", + "solid": "Solide", + "crosshatch": "Kreuzschraffur", + "horizontal": "Horizontal" + }, + "filter": { + "apply": "Anwenden", + "reset": "Zurücksetzen", + "cancel": "Abbrechen", + "spandrel_filter": { + "label": "Bild-zu-Bild Modell", + "description": "Ein Bild-zu-Bild Modell auf der ausgewählten Ebene ausführen.", + "model": "Modell" + }, + "filters": "Filter", + "filterType": "Filtertyp", + "filter": "Filter" + }, + "bookmark": "Lesezeichen für Schnell-Umschalten", + "asRasterLayer": "Als $t(controlLayers.rasterLayer)", + "asRasterLayerResize": "Als $t(controlLayers.rasterLayer) (Größe anpassen)", + "rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)", + "rasterLayer_withCount_other": "Rasterebenen", + "newRasterLayer": "Neue $t(controlLayers.rasterLayer)", + "showNonRasterLayers": "Nicht-Rasterebenen anzeigen (Umschalt+H)", + "hideNonRasterLayers": "Nicht-Rasterebenen ausblenden (Umschalt+H)" + }, + "upscaling": { + "creativity": "Kreativität", + "structure": "Struktur", + "scale": "Maßstab" + }, + "auth": { + "login": { + "title": "Einloggen in InvokeAi", + "email": "Email", + "emailPlaceholder": "Email", + "password": "Password", + "passwordPlaceholder": "Password", + "rememberMe": "Eingeloggt bleiben für 7 Tage", + "signIn": "Einloggen", + "signingIn": "einloggen...", + "loginFailed": "Fehler beim einloggen. Daten prüfen.", + "sessionExpired": "Session ist abgelaufen. Bitte erneuert einloggen." + }, + "setup": { + "title": "Willkommen zu InvokeAI", + "subtitle": "Admin Account anlegen um zu starten", + "email": "Email", + "emailPlaceholder": "admin@beispiel.de", + "emailHelper": "Das wird dein Username sein zum einloggen", + "displayName": "Anzeige Name", + "displayNamePlaceholder": "Administrator", + "displayNameHelper": "Der Anzeigename in der Anwendung", + "password": "Password", + "passwordPlaceholder": "Passwort", + "passwordHelper": "Muss mindestens 8 Zeichen lang sein und Großbuchstaben, Kleinbuchstaben und Zahlen enthalten", + "passwordTooShort": "Das Passwort muss mindestens 8 Zeichen lang sein", + "passwordMissingRequirements": "Das Passwort muss Großbuchstaben, Kleinbuchstaben und Zahlen enthalten", + "confirmPassword": "Passwort bestätigen", + "confirmPasswordPlaceholder": "Passwort bestätigen", + "passwordsDoNotMatch": "Die Passwörter stimmen nicht überein", + "createAccount": "Administratorkonto erstellen", + "creatingAccount": "Einrichtung läuft...", + "setupFailed": "Die Einrichtung ist fehlgeschlagen. Bitte versuchen Sie es erneut.", + "passwordHelperRelaxed": "Geben Sie ein Passwort ein (die Stärke wird angezeigt)" + }, + "userMenu": "User Menü", + "admin": "Admin", + "logout": "Ausloggen", + "adminOnlyFeature": "Diese Funktion steht nur Administratoren zur Verfügung.", + "profile": { + "menuItem": "Mein Profile", + "title": "Mein Profile", + "email": "Email", + "emailReadOnly": "Die E-Mail-Adresse kann nicht geändert werden", + "displayName": "Anzeige Name", + "displayNamePlaceholder": "Dein Name", + "changePassword": "Passwort ändern", + "currentPassword": "Aktuelle Passwort", + "currentPasswordPlaceholder": "Aktuelles Passwort", + "newPassword": "Neues Passwort", + "newPasswordPlaceholder": "Neues Passwort", + "confirmPassword": "Neues Passwort bestätigen", + "confirmPasswordPlaceholder": "Neues Passwort bestätigen", + "passwordsDoNotMatch": "Die Passwörter stimmen nicht überein", + "saveSuccess": "Profil erfolgreich aktualisiert", + "saveFailed": "Profil konnte nicht gespeichert werden. Bitte versuchen Sie es erneut." + }, + "userManagement": { + "menuItem": "Benutzerverwaltung", + "title": "Benutzerverwaltung", + "email": "Email", + "emailPlaceholder": "user@beispiel.de", + "displayName": "Anzeige Name", + "displayNamePlaceholder": "Anzeige Name", + "password": "Passwort", + "passwordPlaceholder": "Passwort", + "newPassword": "Neues Passwort", + "newPasswordPlaceholder": "Lassen Sie dieses Feld leer, um das aktuelle Passwort beizubehalten", + "role": "Rolle", + "status": "Status", + "actions": "Aktionen", + "isAdmin": "Administrator", + "user": "Benutzer", + "you": "Du", + "createUser": "Benutzer anlegen", + "editUser": "Benutzer bearbeiten", + "deleteUser": "Benutzer löschen", + "deleteConfirm": "Möchten Sie \"{{name}}\" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.", + "generatePassword": "Generieren sicheres Passwort", + "showPassword": "Passwort zeigen", + "hidePassword": "Passwort verstecken", + "activate": "Aktivieren", + "deactivate": "Deaktivieren", + "saveFailed": "Benutzer konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.", + "deleteFailed": "Benutzer konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.", + "loadFailed": "Benutzer konnten nicht geladen werden.", + "back": "Zurück", + "cannotDeleteSelf": "Sie können Ihr eigenes Konto nicht löschen", + "cannotDeactivateSelf": "Sie können Ihr eigenes Konto nicht löschen" + }, + "passwordStrength": { + "weak": "schwaches Passwort", + "moderate": "Moderates Passwort", + "strong": "Starkes Passwort" + } + } +} diff --git a/invokeai/frontend/web/public/locales/en-GB.json b/invokeai/frontend/web/public/locales/en-GB.json new file mode 100644 index 00000000000..c6bbc13e434 --- /dev/null +++ b/invokeai/frontend/web/public/locales/en-GB.json @@ -0,0 +1,7 @@ +{ + "accessibility": { + "about": "About", + "createIssue": "Create Issue", + "submitSupportTicket": "Submit Support Ticket" + } +} diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json new file mode 100644 index 00000000000..75367a502db --- /dev/null +++ b/invokeai/frontend/web/public/locales/en.json @@ -0,0 +1,3544 @@ +{ + "accessibility": { + "about": "About", + "createIssue": "Create Issue", + "submitSupportTicket": "Submit Support Ticket", + "invokeProgressBar": "Invoke progress bar", + "menu": "Menu", + "mode": "Mode", + "nextImage": "Next Image", + "previousImage": "Previous Image", + "reset": "Reset", + "resetUI": "$t(accessibility.reset) UI", + "toggleRightPanel": "Toggle Right Panel (G)", + "toggleLeftPanel": "Toggle Left Panel (T)", + "uploadImage": "Upload Image", + "uploadImages": "Upload Images" + }, + "auth": { + "login": { + "title": "Sign In to InvokeAI", + "email": "Email", + "emailPlaceholder": "Email", + "password": "Password", + "passwordPlaceholder": "Password", + "rememberMe": "Remember me for 7 days", + "signIn": "Sign In", + "signingIn": "Signing in...", + "loginFailed": "Login failed. Please check your credentials.", + "sessionExpired": "Your credentials have expired. Please log in again to resume." + }, + "setup": { + "title": "Welcome to InvokeAI", + "subtitle": "Set up your administrator account to get started", + "email": "Email", + "emailPlaceholder": "admin@example.com", + "emailHelper": "This will be your username for signing in", + "displayName": "Display Name", + "displayNamePlaceholder": "Administrator", + "displayNameHelper": "Your name as it will appear in the application", + "password": "Password", + "passwordPlaceholder": "Password", + "passwordHelper": "Must be at least 8 characters with uppercase, lowercase, and numbers", + "passwordTooShort": "Password must be at least 8 characters long", + "passwordMissingRequirements": "Password must contain uppercase, lowercase, and numbers", + "confirmPassword": "Confirm Password", + "confirmPasswordPlaceholder": "Confirm Password", + "passwordsDoNotMatch": "Passwords do not match", + "createAccount": "Create Administrator Account", + "creatingAccount": "Setting up...", + "setupFailed": "Setup failed. Please try again.", + "passwordHelperRelaxed": "Enter any password (strength will be shown)" + }, + "userMenu": "User Menu", + "admin": "Admin", + "logout": "Logout", + "adminOnlyFeature": "This feature is only available to administrators.", + "profile": { + "menuItem": "My Profile", + "title": "My Profile", + "email": "Email", + "emailReadOnly": "Email address cannot be changed", + "displayName": "Display Name", + "displayNamePlaceholder": "Your name", + "changePassword": "Change Password", + "currentPassword": "Current Password", + "currentPasswordPlaceholder": "Current password", + "newPassword": "New Password", + "newPasswordPlaceholder": "New password", + "confirmPassword": "Confirm New Password", + "confirmPasswordPlaceholder": "Confirm new password", + "passwordsDoNotMatch": "Passwords do not match", + "saveSuccess": "Profile updated successfully", + "saveFailed": "Failed to save profile. Please try again." + }, + "userManagement": { + "menuItem": "User Management", + "title": "User Management", + "email": "Email", + "emailPlaceholder": "user@example.com", + "displayName": "Display Name", + "displayNamePlaceholder": "Display name", + "password": "Password", + "passwordPlaceholder": "Password", + "newPassword": "New Password", + "newPasswordPlaceholder": "Leave blank to keep current password", + "role": "Role", + "status": "Status", + "actions": "Actions", + "isAdmin": "Administrator", + "user": "User", + "you": "You", + "createUser": "Create User", + "editUser": "Edit User", + "deleteUser": "Delete User", + "deleteConfirm": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.", + "generatePassword": "Generate Strong Password", + "showPassword": "Show password", + "hidePassword": "Hide password", + "activate": "Activate", + "deactivate": "Deactivate", + "saveFailed": "Failed to save user. Please try again.", + "deleteFailed": "Failed to delete user. Please try again.", + "loadFailed": "Failed to load users.", + "back": "Back", + "cannotDeleteSelf": "You cannot delete your own account", + "cannotDeactivateSelf": "You cannot deactivate your own account" + }, + "passwordStrength": { + "weak": "Weak password", + "moderate": "Moderate password", + "strong": "Strong password" + } + }, + "boards": { + "addBoard": "Add Board", + "addPrivateBoard": "Add Private Board", + "addSharedBoard": "Add Shared Board", + "archiveBoard": "Archive Board", + "archived": "Archived", + "autoAddBoard": "Auto-Add Board", + "boards": "Boards", + "selectedForAutoAdd": "Selected for Auto-Add", + "bottomMessage": "Deleting images will reset any features currently using them.", + "cancel": "Cancel", + "pause": "Pause", + "resume": "Resume", + "restartFailed": "Restart failed", + "restartFile": "Restart file", + "restartRequired": "Restart required", + "resumeRefused": "Resume refused by server. Restart required.", + "changeBoard": "Change Board", + "clearSearch": "Clear Search", + "deleteBoard": "Delete Board", + "deleteBoardAndImages": "Delete Board and Images", + "deleteBoardOnly": "Delete Board Only", + "deletedBoardsCannotbeRestored": "Deleted boards and images cannot be restored. Selecting 'Delete Board Only' will move images to an uncategorized state.", + "deletedPrivateBoardsCannotbeRestored": "Deleted boards and images cannot be restored. Selecting 'Delete Board Only' will move images to a private uncategorized state for the image's creator.", + "uncategorizedImages": "Uncategorized Images", + "deleteAllUncategorizedImages": "Delete All Uncategorized Images", + "deletedImagesCannotBeRestored": "Deleted images cannot be restored.", + "hideBoards": "Hide Boards", + "loading": "Loading...", + "locateInGalery": "Locate in Gallery", + "menuItemAutoAdd": "Auto-add to this Board", + "move": "Move", + "movingImagesToBoard_one": "Moving {{count}} image to board:", + "movingImagesToBoard_other": "Moving {{count}} images to board:", + "myBoard": "My Board", + "noBoards": "No {{boardType}} Boards", + "noMatching": "No matching Boards", + "private": "Private Boards", + "searchBoard": "Search Boards...", + "selectBoard": "Select a Board", + "shared": "Shared Boards", + "topMessage": "This selection contains images used in the following features:", + "unarchiveBoard": "Unarchive Board", + "uncategorized": "Uncategorized", + "viewBoards": "View Boards", + "downloadBoard": "Download Board", + "imagesWithCount_one": "{{count}} image", + "imagesWithCount_other": "{{count}} images", + "assetsWithCount_one": "{{count}} asset", + "assetsWithCount_other": "{{count}} assets", + "updateBoardError": "Error updating board", + "setBoardVisibility": "Set Board Visibility", + "setVisibilityPrivate": "Set Private", + "setVisibilityShared": "Set Shared", + "setVisibilityPublic": "Set Public", + "visibilityPrivate": "Private", + "visibilityShared": "Shared", + "visibilityPublic": "Public", + "visibilityBadgeShared": "Shared board", + "visibilityBadgePublic": "Public board", + "updateBoardVisibilityError": "Error updating board visibility" + }, + "accordions": { + "generation": { + "title": "Generation" + }, + "image": { + "title": "Image" + }, + "advanced": { + "title": "Advanced", + "options": "$t(accordions.advanced.title) Options" + }, + "control": { + "title": "Control" + }, + "compositing": { + "title": "Compositing", + "coherenceTab": "Coherence Pass", + "infillTab": "Infill" + } + }, + "common": { + "aboutDesc": "Using Invoke for work? Check out:", + "aboutHeading": "Own Your Creative Power", + "accept": "Accept", + "apply": "Apply", + "add": "Add", + "advanced": "Advanced", + "ai": "ai", + "areYouSure": "Are you sure?", + "auto": "Auto", + "back": "Back", + "batch": "Batch Manager", + "beta": "Beta", + "board": "Board", + "cancel": "Cancel", + "close": "Close", + "copy": "Copy", + "copyError": "$t(gallery.copy) Error", + "clipboard": "Clipboard", + "collapseAll": "Collapse All", + "crop": "Crop", + "on": "On", + "off": "Off", + "or": "or", + "ok": "Ok", + "checkpoint": "Checkpoint", + "communityLabel": "Community", + "controlNet": "ControlNet", + "data": "Data", + "delete": "Delete", + "details": "Details", + "direction": "Direction", + "ipAdapter": "IP Adapter", + "t2iAdapter": "T2I Adapter", + "prompt": "Prompt", + "positivePrompt": "Positive Prompt", + "negativePrompt": "Negative Prompt", + "removeNegativePrompt": "Remove Negative Prompt", + "addNegativePrompt": "Add Negative Prompt", + "selectYourModel": "Select Your Model", + "discordLabel": "Discord", + "dontAskMeAgain": "Don't ask me again", + "dontShowMeThese": "Don't show me these", + "editName": "Edit name", + "editor": "Editor", + "error": "Error", + "error_withCount_one": "{{count}} error", + "error_withCount_other": "{{count}} errors", + "expandAll": "Expand All", + "model_withCount_one": "{{count}} model", + "model_withCount_other": "{{count}} models", + "file": "File", + "fitView": "Fit View", + "folder": "Folder", + "format": "format", + "githubLabel": "Github", + "goTo": "Go to", + "hotkeysLabel": "Hotkeys", + "hex": "Hex", + "imageFailedToLoad": "Unable to Load Image", + "img2img": "Image To Image", + "inpaint": "inpaint", + "input": "Input", + "installed": "Installed", + "json": "JSON", + "languagePickerLabel": "Language", + "linear": "Linear", + "load": "Load", + "loading": "Loading", + "loadingImage": "Loading Image", + "loadingModel": "Loading Model", + "localSystem": "Local System", + "minimize": "Minimize", + "next": "Next", + "noMatchingItems": "No matching items", + "notifications": "Notifications", + "learnMore": "Learn More", + "modelManager": "Model Manager", + "noMatches": "No matches", + "noOptions": "No options", + "nodes": "Workflows", + "notInstalled": "Not $t(common.installed)", + "openSlider": "Open slider", + "openInNewTab": "Open in New Tab", + "openInViewer": "Open in Viewer", + "orderBy": "Order By", + "outpaint": "outpaint", + "outputs": "Outputs", + "postprocessing": "Post Processing", + "previous": "Previous", + "random": "Random", + "removeFromCollection": "Remove from Collection", + "reportBugLabel": "Report Bug", + "resetView": "Reset View", + "safetensors": "Safetensors", + "save": "Save", + "saveAs": "Save As", + "saveChanges": "Save Changes", + "saveToAssets": "Save to Assets", + "settings": "Settings", + "settingsLabel": "Settings", + "simple": "Simple", + "somethingWentWrong": "Something went wrong", + "statusDisconnected": "Disconnected", + "template": "Template", + "toggleRgbHex": "Toggle RGB/HEX", + "toResolve": "To resolve", + "txt2img": "Text To Image", + "unknown": "Unknown", + "unpin": "Unpin", + "upload": "Upload", + "userGuideLabel": "User Guide", + "zoomIn": "Zoom In", + "zoomOut": "Zoom Out", + "updated": "Updated", + "created": "Created", + "prevPage": "Previous Page", + "nextPage": "Next Page", + "unknownError": "Unknown Error", + "red": "Red", + "green": "Green", + "blue": "Blue", + "alpha": "Alpha", + "selected": "Selected", + "search": "Search", + "clear": "Clear", + "tab": "Tab", + "view": "View", + "edit": "Edit", + "enabled": "Enabled", + "disabled": "Disabled", + "placeholderSelectAModel": "Select a model", + "reset": "Reset", + "none": "None", + "new": "New", + "generating": "Generating", + "warnings": "Warnings", + "start": "Start", + "count": "Count", + "step": "Step", + "end": "End", + "min": "Min", + "max": "Max", + "values": "Values", + "resetToDefaults": "Reset to Defaults", + "seed": "Seed", + "combinatorial": "Combinatorial", + "layout": "Layout", + "row": "Row", + "column": "Column", + "value": "Value", + "label": "Label", + "systemInformation": "System Information", + "compactView": "Compact View", + "fullView": "Full View", + "options_withCount_one": "{{count}} option", + "options_withCount_other": "{{count}} options" + }, + "hrf": { + "hrf": "High Resolution Fix", + "enableHrf": "Enable High Resolution Fix", + "upscaleMethod": "Upscale Method", + "metadata": { + "enabled": "High Resolution Fix Enabled", + "strength": "High Resolution Fix Strength", + "method": "High Resolution Fix Method" + } + }, + "prompt": { + "addPromptTrigger": "Add Prompt Trigger", + "compatibleEmbeddings": "Compatible Embeddings", + "noMatchingTriggers": "No matching triggers", + "generateFromImage": "Generate prompt from image", + "expandCurrentPrompt": "Expand Current Prompt", + "uploadImageForPromptGeneration": "Upload Image for Prompt Generation", + "expandingPrompt": "Expanding prompt...", + "resultTitle": "Prompt Expansion Complete", + "resultSubtitle": "Choose how to handle the expanded prompt:", + "replace": "Replace", + "insert": "Insert", + "discard": "Discard", + "noPromptHistory": "No prompt history recorded.", + "noMatchingPrompts": "No matching prompts in history.", + "toSwitchBetweenPrompts": "to switch between prompts.", + "promptHistory": "Prompt History", + "clearHistory": "Clear History", + "usePrompt": "Use prompt", + "searchPrompts": "Search...", + "imageToPrompt": "Image to Prompt", + "selectVisionModel": "Select Vision Model...", + "changeImage": "Change Image", + "uploadImage": "Upload Image", + "generatePrompt": "Generate Prompt", + "expandPromptWithLLM": "Expand Prompt with LLM", + "expandPrompt": "Expand Prompt", + "selectTextLLM": "Select Text LLM...", + "expand": "Expand", + "noTextLLMInstalledTitle": "No Text LLM installed", + "noTextLLMInstalledDescription": "Prompt expansion needs a Text LLM (causal language model). We recommend Qwen2.5-1.5B-Instruct (~3 GB) — small, fast, and available as a starter model.", + "noVisionModelInstalledTitle": "No vision model installed", + "noVisionModelInstalledDescription": "Image-to-prompt needs a vision-language model (e.g. LLaVA Onevision). The 0.5B starter (~1 GB) is the lightweight default.", + "openModelManager": "Open Model Manager" + }, + "queue": { + "queue": "Queue", + "queueFront": "Add to Front of Queue", + "queueBack": "Add to Queue", + "queueActionsMenu": "Queue Actions Menu", + "queueEmpty": "Queue Empty", + "queueItem": "Queue Item", + "enqueueing": "Queueing Batch", + "resume": "Resume", + "resumeTooltip": "Resume Processor", + "resumeSucceeded": "Processor Resumed", + "resumeFailed": "Problem Resuming Processor", + "pause": "Pause", + "pauseTooltip": "Pause Processor", + "pauseSucceeded": "Processor Paused", + "pauseFailed": "Problem Pausing Processor", + "cancel": "Cancel", + "cancelAllExceptCurrentQueueItemAlertDialog": "Canceling all queue items except the current one will stop pending items but allow the in-progress one to finish.", + "cancelAllExceptCurrentQueueItemAlertDialog2": "Are you sure you want to cancel all pending queue items?", + "cancelAllExceptCurrent": "Cancel All Except Current", + "cancelAllExceptCurrentTooltip": "Cancel All Except Current Item", + "cancelTooltip": "Cancel Current Item", + "cancelSucceeded": "Item Canceled", + "cancelFailed": "Problem Canceling Item", + "cancelFailedAccessDenied": "Problem Canceling Item: Access Denied", + "retrySucceeded": "Item Retried", + "retryFailed": "Problem Retrying Item", + "confirm": "Confirm", + "prune": "Prune", + "pruneTooltip": "Prune {{item_count}} Completed Items", + "pruneSucceeded": "Pruned {{item_count}} Completed Items from Queue", + "pruneFailed": "Problem Pruning Queue", + "clear": "Clear", + "clearTooltip": "Cancel and Clear All Items", + "clearSucceeded": "Queue Cleared", + "clearFailed": "Problem Clearing Queue", + "clearFailedAccessDenied": "Problem Clearing Queue: Access Denied", + "cancelBatch": "Cancel Batch", + "cancelItem": "Cancel Item", + "retryItem": "Retry Item", + "cancelBatchSucceeded": "Batch Canceled", + "cancelBatchFailed": "Problem Canceling Batch", + "clearQueueAlertDialog": "Clearing the queue immediately cancels any processing items and clears the queue entirely. Pending filters will be canceled and the Canvas Staging Area will be reset.", + "clearQueueAlertDialog2": "Are you sure you want to clear the queue?", + "current": "Current", + "next": "Next", + "status": "Status", + "total": "Total", + "time": "Time", + "credits": "Credits", + "pending": "Pending", + "in_progress": "In Progress", + "paused": "Paused", + "completed": "Completed", + "failed": "Failed", + "canceled": "Canceled", + "completedIn": "Completed in", + "batch": "Batch", + "user": "User", + "origin": "Origin", + "destination": "Dest", + "upscaling": "Upscaling", + "canvas": "Canvas", + "generation": "Generation", + "workflows": "Workflows", + "other": "Other", + "gallery": "Gallery", + "batchFieldValues": "Batch Field Values", + "fieldValuesHidden": "", + "cannotViewDetails": "You do not have permission to view the details of this queue item", + "item": "Item", + "session": "Session", + "notReady": "Unable to Queue", + "batchQueued": "Batch Queued", + "batchQueuedDesc_one": "Added {{count}} sessions to {{direction}} of queue", + "batchQueuedDesc_other": "Added {{count}} sessions to {{direction}} of queue", + "front": "front", + "back": "back", + "batchFailedToQueue": "Failed to Queue Batch", + "graphQueued": "Graph queued", + "graphFailedToQueue": "Failed to queue graph", + "openQueue": "Open Queue", + "prompts_one": "Prompt", + "prompts_other": "Prompts", + "iterations_one": "Iteration", + "iterations_other": "Iterations", + "generations_one": "Generation", + "generations_other": "Generations", + "batchSize": "Batch Size", + "createdAt": "Created At", + "completedAt": "Completed At", + "sortColumn": "Sort Column", + "sortBy": "Sort by {{column}}", + "sortOrderAscending": "Ascending", + "sortOrderDescending": "Descending" + }, + "invocationCache": { + "invocationCache": "Invocation Cache", + "cacheSize": "Cache Size", + "maxCacheSize": "Max Cache Size", + "hits": "Cache Hits", + "misses": "Cache Misses", + "clear": "Clear", + "clearSucceeded": "Invocation Cache Cleared", + "clearFailed": "Problem Clearing Invocation Cache", + "enable": "Enable", + "enableSucceeded": "Invocation Cache Enabled", + "enableFailed": "Problem Enabling Invocation Cache", + "disable": "Disable", + "disableSucceeded": "Invocation Cache Disabled", + "disableFailed": "Problem Disabling Invocation Cache", + "useCache": "Use Cache" + }, + "modelCache": { + "clear": "Clear Model Cache", + "clearSucceeded": "Model Cache Cleared", + "clearFailed": "Problem Clearing Model Cache" + }, + "gallery": { + "gallery": "Gallery", + "images": "Images", + "assets": "Assets", + "alwaysShowImageSizeBadge": "Always Show Image Size Badge", + "assetsTab": "Files you've uploaded for use in your projects.", + "autoAssignBoardOnClick": "Auto-Assign Board", + "autoSwitchNewImages": "Auto-Switch to New Images", + "boardsSettings": "Boards Settings", + "copy": "Copy", + "currentlyInUse": "This image is currently in use in the following features:", + "drop": "Drop", + "dropOrUpload": "Drop or Upload", + "dropToUpload": "$t(gallery.drop) to Upload", + "deleteImage_one": "Delete Image", + "deleteImage_other": "Delete {{count}} Images", + "deleteImagePermanent": "Deleted images cannot be restored.", + "displayBoardSearch": "Board Search", + "displaySearch": "Image Search", + "download": "Download", + "exitBoardSearch": "Exit Board Search", + "exitSearch": "Exit Image Search", + "featuresWillReset": "If you delete this image, those features will immediately be reset.", + "galleryImageSize": "Image Size", + "gallerySettings": "Gallery Settings", + "go": "Go", + "image": "image", + "imagesTab": "Images you've created and saved within Invoke.", + "imagesSettings": "Gallery Images Settings", + "jump": "Jump", + "loading": "Loading", + "loadingGallery": "Loading gallery...", + "loadingMetadata": "Loading metadata...", + "newestFirst": "Newest First", + "noImagesFound": "No images found", + "oldestFirst": "Oldest First", + "sortDirection": "Sort Direction", + "showStarredImagesFirst": "Show Starred Images First", + "usePagedGalleryView": "Use Paged Gallery View", + "noImageSelected": "No Image Selected", + "noImagesInGallery": "No Images to Display", + "starImage": "Star", + "unstarImage": "Unstar", + "unableToLoad": "Unable to load Gallery", + "deleteSelection": "Delete Selection", + "downloadSelection": "Download Selection", + "bulkDownloadReady": "Download ready", + "clickToDownload": "Click here to download", + "bulkDownloadRequested": "Preparing Download", + "bulkDownloadRequestedDesc": "Your download request is being prepared. This may take a few moments.", + "bulkDownloadRequestFailed": "Problem Preparing Download", + "bulkDownloadFailed": "Download Failed", + "viewerImage": "Viewer Image", + "compareImage": "Compare Image", + "openInViewer": "Open in Viewer", + "searchImages": "Search by Metadata", + "selectAllOnPage": "Select All On Page", + "showArchivedBoards": "Show Archived Boards", + "selectForCompare": "Select for Compare", + "selectAnImageToCompare": "Select an Image to Compare", + "slider": "Slider", + "sideBySide": "Side-by-Side", + "hover": "Hover", + "swapImages": "Swap Images", + "stretchToFit": "Stretch to Fit", + "exitCompare": "Exit Compare", + "compareHelp1": "Hold Alt while clicking a gallery image or using the arrow keys to change the compare image.", + "compareHelp2": "Press M to cycle through comparison modes.", + "compareHelp3": "Press C to swap the compared images.", + "compareHelp4": "Press Z or Esc to exit.", + "openViewer": "Open Viewer", + "closeViewer": "Close Viewer", + "move": "Move", + "useForPromptGeneration": "Use for Prompt Generation" + }, + "hotkeys": { + "hotkeys": "Hotkeys", + "searchHotkeys": "Search Hotkeys", + "clearSearch": "Clear Search", + "noHotkeysFound": "No Hotkeys Found", + "editMode": "Edit Mode", + "viewMode": "View Mode", + "editHotkey": "Edit Hotkey", + "addHotkey": "Add Hotkey", + "resetToDefault": "Reset to Default", + "resetAll": "Reset All to Default", + "resetAllConfirmation": "Are you sure you want to reset all hotkeys to their default values? This cannot be undone.", + "enterHotkeys": "Enter hotkeys, separated by commas", + "save": "Save", + "cancel": "Cancel", + "modifiers": "Modifiers", + "syntaxHelp": "Syntax Help", + "combineWith": "Combine with +", + "multipleHotkeys": "Multiple hotkeys with comma", + "validKeys": "Valid keys", + "help": "Help", + "noHotkeysRecorded": "No hotkeys recorded yet", + "pressKeys": "Press keys...", + "setHotkey": "SET", + "setAnother": "SET ANOTHER", + "removeLastHotkey": "Remove last hotkey", + "clearAll": "Clear All", + "duplicateWarning": "This hotkey is already recorded", + "conflictWarning": "is already used by \"{{hotkeyTitle}}\"", + "thisHotkey": "this hotkey", + "app": { + "title": "App", + "invoke": { + "title": "Invoke", + "desc": "Queue a generation, adding it to the end of the queue." + }, + "invokeFront": { + "title": "Invoke (Front)", + "desc": "Queue a generation, adding it to the front of the queue." + }, + "cancelQueueItem": { + "title": "Cancel", + "desc": "Cancel the currently processing queue item." + }, + "clearQueue": { + "title": "Clear Queue", + "desc": "Cancel and clear all queue items." + }, + "selectCanvasTab": { + "title": "Select the Canvas Tab", + "desc": "Selects the Canvas tab." + }, + "selectUpscalingTab": { + "title": "Select the Upscaling Tab", + "desc": "Selects the Upscaling tab." + }, + "selectWorkflowsTab": { + "title": "Select the Workflows Tab", + "desc": "Selects the Workflows tab." + }, + "selectModelsTab": { + "title": "Select the Models Tab", + "desc": "Selects the Models tab." + }, + "selectQueueTab": { + "title": "Select the Queue Tab", + "desc": "Selects the Queue tab." + }, + "focusPrompt": { + "title": "Focus Prompt", + "desc": "Move cursor focus to the positive prompt." + }, + "promptHistoryPrev": { + "title": "Previous Prompt in History", + "desc": "When the prompt is focused, move to the previous (older) prompt in your history." + }, + "promptHistoryNext": { + "title": "Next Prompt in History", + "desc": "When the prompt is focused, move to the next (newer) prompt in your history." + }, + "promptWeightUp": { + "title": "Increase Weight of Prompt Selection", + "desc": "When the prompt is focused and text is selected, increase the weight of the selected prompt." + }, + "promptWeightDown": { + "title": "Decrease Weight of Prompt Selection", + "desc": "When the prompt is focused and text is selected, decrease the weight of the selected prompt." + }, + "toggleLeftPanel": { + "title": "Toggle Left Panel", + "desc": "Show or hide the left panel." + }, + "toggleRightPanel": { + "title": "Toggle Right Panel", + "desc": "Show or hide the right panel." + }, + "resetPanelLayout": { + "title": "Reset Panel Layout", + "desc": "Reset the left and right panels to their default size and layout." + }, + "togglePanels": { + "title": "Toggle Panels", + "desc": "Show or hide both left and right panels at once." + }, + "selectGenerateTab": { + "title": "Select the Generate Tab", + "desc": "Selects the Generate tab.", + "key": "1" + } + }, + "canvas": { + "title": "Canvas", + "selectBrushTool": { + "title": "Brush Tool", + "desc": "Select the brush tool." + }, + "selectBboxTool": { + "title": "Bbox Tool", + "desc": "Select the bounding box tool." + }, + "decrementToolWidth": { + "title": "Decrement Tool Width", + "desc": "Decrement the brush or eraser tool width, whichever is selected." + }, + "incrementToolWidth": { + "title": "Increment Tool Width", + "desc": "Increment the brush or eraser tool width, whichever is selected." + }, + "selectColorPickerTool": { + "title": "Color Picker Tool", + "desc": "Select the color picker tool." + }, + "selectEraserTool": { + "title": "Eraser Tool", + "desc": "Select the eraser tool." + }, + "selectMoveTool": { + "title": "Move Tool", + "desc": "Select the move tool." + }, + "selectRectTool": { + "title": "Shapes Tool", + "desc": "Select the shapes tool." + }, + "selectLassoTool": { + "title": "Lasso Tool", + "desc": "Select the lasso tool." + }, + "selectViewTool": { + "title": "View Tool", + "desc": "Select the view tool." + }, + "fitLayersToCanvas": { + "title": "Fit Layers to Canvas", + "desc": "Scale and position the view to fit all visible layers." + }, + "fitBboxToCanvas": { + "title": "Fit Bbox to Canvas", + "desc": "Scale and position the view to fit the bbox." + }, + "setZoomTo100Percent": { + "title": "Zoom to 100%", + "desc": "Set the canvas zoom to 100%." + }, + "setZoomTo200Percent": { + "title": "Zoom to 200%", + "desc": "Set the canvas zoom to 200%." + }, + "setZoomTo400Percent": { + "title": "Zoom to 400%", + "desc": "Set the canvas zoom to 400%." + }, + "setZoomTo800Percent": { + "title": "Zoom to 800%", + "desc": "Set the canvas zoom to 800%." + }, + "quickSwitch": { + "title": "Layer Quick Switch", + "desc": "Switch between the last two selected layers. If a layer is bookmarked, always switch between it and the last non-bookmarked layer." + }, + "deleteSelected": { + "title": "Delete Layer", + "desc": "Delete the selected layer." + }, + "resetSelected": { + "title": "Reset Layer", + "desc": "Reset the selected layer. Only applies to Inpaint Mask and Regional Guidance." + }, + "mergeDown": { + "title": "Merge Layer Down", + "desc": "Merge the selected layer into the layer directly below it." + }, + "mergeVisible": { + "title": "Merge All Visible Layers", + "desc": "Merge all visible layers of the selected layer type." + }, + "undo": { + "title": "Undo", + "desc": "Undo the last canvas action." + }, + "redo": { + "title": "Redo", + "desc": "Redo the last canvas action." + }, + "nextEntity": { + "title": "Next Layer", + "desc": "Select the next layer in the list." + }, + "prevEntity": { + "title": "Prev Layer", + "desc": "Select the previous layer in the list." + }, + "setFillColorsToDefault": { + "title": "Set Colors to Default", + "desc": "Set the current tool colors to default." + }, + "toggleFillColor": { + "title": "Toggle Fill Color", + "desc": "Toggle the current tool fill color." + }, + "filterSelected": { + "title": "Filter", + "desc": "Filter the selected layer. Only applies to Raster and Control layers." + }, + "transformSelected": { + "title": "Transform", + "desc": "Transform the selected layer." + }, + "invertMask": { + "title": "Invert Mask", + "desc": "Invert the selected inpaint mask, creating a new mask with opposite transparency." + }, + "applyFilter": { + "title": "Apply Filter", + "desc": "Apply the pending filter to the selected layer." + }, + "cancelFilter": { + "title": "Cancel Filter", + "desc": "Cancel the pending filter." + }, + "applyTransform": { + "title": "Apply Transform", + "desc": "Apply the pending transform to the selected layer." + }, + "cancelTransform": { + "title": "Cancel Transform", + "desc": "Cancel the pending transform." + }, + "settings": { + "behavior": "Behavior", + "display": "Display", + "grid": "Grid", + "debug": "Debug" + }, + "toggleNonRasterLayers": { + "title": "Toggle Non-Raster Layers", + "desc": "Show or hide all non-raster layer categories (Control Layers, Inpaint Masks, Regional Guidance)." + }, + "fitBboxToLayers": { + "title": "Fit Bbox To Layers", + "desc": "Automatically adjust the generation bounding box to fit visible layers" + }, + "fitBboxToMasks": { + "title": "Fit Bbox To Masks", + "desc": "Automatically adjust the generation bounding box to fit visible inpaint masks" + }, + "toggleBbox": { + "title": "Toggle Bbox Visibility", + "desc": "Hide or show the generation bounding box" + }, + "applySegmentAnything": { + "title": "Apply Segment Anything", + "desc": "Apply the current Segment Anything mask.", + "key": "enter" + }, + "cancelSegmentAnything": { + "title": "Cancel Segment Anything", + "desc": "Cancel the current Segment Anything operation.", + "key": "esc" + } + }, + "workflows": { + "title": "Workflows", + "addNode": { + "title": "Add Node", + "desc": "Open the add node menu." + }, + "copySelection": { + "title": "Copy", + "desc": "Copy selected nodes and edges." + }, + "pasteSelection": { + "title": "Paste", + "desc": "Paste copied nodes and edges." + }, + "pasteSelectionWithEdges": { + "title": "Paste with Edges", + "desc": "Paste copied nodes, edges, and all edges connected to copied nodes." + }, + "selectAll": { + "title": "Select All", + "desc": "Select all nodes and edges." + }, + "deleteSelection": { + "title": "Delete", + "desc": "Delete selected nodes and edges." + }, + "undo": { + "title": "Undo", + "desc": "Undo the last workflow action." + }, + "redo": { + "title": "Redo", + "desc": "Redo the last workflow action." + } + }, + "viewer": { + "title": "Image Viewer", + "toggleViewer": { + "title": "Show/Hide Image Viewer", + "desc": "Show or hide the image viewer. Only available on the Canvas tab." + }, + "swapImages": { + "title": "Swap Comparison Images", + "desc": "Swap the images being compared." + }, + "nextComparisonMode": { + "title": "Next Comparison Mode", + "desc": "Cycle through comparison modes." + }, + "loadWorkflow": { + "title": "Load Workflow", + "desc": "Load the current image's saved workflow (if it has one)." + }, + "recallAll": { + "title": "Recall All Metadata", + "desc": "Recall all metadata for the current image." + }, + "recallSeed": { + "title": "Recall Seed", + "desc": "Recall the seed for the current image." + }, + "recallPrompts": { + "title": "Recall Prompts", + "desc": "Recall the positive and negative prompts for the current image." + }, + "remix": { + "title": "Remix", + "desc": "Recall all metadata except for the seed for the current image." + }, + "useSize": { + "title": "Use Size", + "desc": "Use the current image's size as the bbox size." + }, + "runPostprocessing": { + "title": "Run Postprocessing", + "desc": "Run the selected postprocessing on the current image." + }, + "toggleMetadata": { + "title": "Show/Hide Metadata", + "desc": "Show or hide the current image's metadata overlay." + } + }, + "gallery": { + "title": "Gallery", + "selectAllOnPage": { + "title": "Select All On Page", + "desc": "Select all images on the current page." + }, + "clearSelection": { + "title": "Clear Selection", + "desc": "Clear the current selection, if any." + }, + "galleryNavUp": { + "title": "Navigate Up", + "desc": "Navigate up in the gallery grid, selecting that image. If at the top of the page, go to the previous page." + }, + "galleryNavRight": { + "title": "Navigate Right", + "desc": "Navigate right in the gallery grid, selecting that image. If at the last image of the row, go to the next row. If at the last image of the page, go to the next page." + }, + "galleryNavDown": { + "title": "Navigate Down", + "desc": "Navigate down in the gallery grid, selecting that image. If at the bottom of the page, go to the next page." + }, + "galleryNavLeft": { + "title": "Navigate Left", + "desc": "Navigate left in the gallery grid, selecting that image. If at the first image of the row, go to the previous row. If at the first image of the page, go to the previous page." + }, + "galleryNavUpAlt": { + "title": "Navigate Up (Compare Image)", + "desc": "Same as Navigate Up, but selects the compare image, opening compare mode if it isn't already open." + }, + "galleryNavRightAlt": { + "title": "Navigate Right (Compare Image)", + "desc": "Same as Navigate Right, but selects the compare image, opening compare mode if it isn't already open." + }, + "galleryNavDownAlt": { + "title": "Navigate Down (Compare Image)", + "desc": "Same as Navigate Down, but selects the compare image, opening compare mode if it isn't already open." + }, + "galleryNavLeftAlt": { + "title": "Navigate Left (Compare Image)", + "desc": "Same as Navigate Left, but selects the compare image, opening compare mode if it isn't already open." + }, + "deleteSelection": { + "title": "Delete", + "desc": "Delete all selected images. By default, you will be prompted to confirm deletion. If the images are currently in use in the app, you will be warned." + }, + "starImage": { + "title": "Star/Unstar Image", + "desc": "Star or unstar the selected image." + } + } + }, + "lora": { + "weight": "Weight", + "removeLoRA": "Remove LoRA" + }, + "metadata": { + "allPrompts": "All Prompts", + "cfgScale": "CFG scale", + "cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)", + "clipSkip": "$t(parameters.clipSkip)", + "createdBy": "Created By", + "dypePreset": "$t(parameters.dypePreset)", + "dypeScale": "$t(parameters.dypeScale)", + "dypeExponent": "$t(parameters.dypeExponent)", + "generationMode": "Generation Mode", + "geminiTemperature": "Gemini Temperature", + "geminiThinkingLevel": "Gemini Thinking Level", + "openaiQuality": "OpenAI Quality", + "openaiBackground": "OpenAI Background", + "openaiInputFidelity": "OpenAI Input Fidelity", + "seedreamWatermark": "Seedream Watermark", + "seedreamOptimizePrompt": "Seedream Optimize Prompt", + "guidance": "Guidance", + "height": "Height", + "imageDetails": "Image Details", + "imageDimensions": "Image Dimensions", + "imageSize": "Image Size", + "metadata": "Metadata", + "model": "Model", + "negativePrompt": "Negative Prompt", + "noImageDetails": "No image details found", + "noMetaData": "No metadata found", + "noRecallParameters": "No parameters to recall found", + "parameterSet": "Parameter {{parameter}} set", + "parsingFailed": "Parsing Failed", + "positivePrompt": "Positive Prompt", + "qwen3Encoder": "Qwen3 Encoder", + "qwen3Source": "Qwen3 Source", + "recallParameters": "Recall Parameters", + "recallParameter": "Recall {{label}}", + "scheduler": "Scheduler", + "seamlessXAxis": "Seamless X Axis", + "seamlessYAxis": "Seamless Y Axis", + "seedVarianceEnabled": "Seed Variance Enabled", + "seedVarianceStrength": "Seed Variance Strength", + "seedVarianceRandomizePercent": "Seed Variance Randomize %", + "zImageShift": "Z-Image Shift", + "seed": "Seed", + "steps": "Steps", + "strength": "Image to image strength", + "Threshold": "Noise Threshold", + "vae": "VAE", + "width": "Width", + "workflow": "Workflow", + "canvasV2Metadata": "Canvas Layers" + }, + "modelManager": { + "active": "active", + "actions": "Bulk Actions", + "deleteModelsConfirm_one": "Are you sure you want to delete {{count}} model? This action cannot be undone.", + "deleteModelsConfirm_other": "Are you sure you want to delete {{count}} models? This action cannot be undone.", + "deleteWarning": "Models in your Invoke models directory will be permanently deleted from disk.", + "modelsDeleted_one": "Successfully deleted {{count}} model", + "modelsDeleted_other": "Successfully deleted {{count}} models", + "modelsDeleteFailed": "Failed to delete models", + "someModelsFailedToDelete_one": "{{count}} model could not be deleted", + "someModelsFailedToDelete_other": "{{count}} models could not be deleted", + "modelsDeletedPartial": "Partially completed", + "someModelsDeleted": "{{deleted}} deleted, {{failed}} failed", + "modelsDeleteError": "Error deleting models", + "pause": "Pause", + "pauseAll": "Pause All", + "pauseAllTooltip": "Pause all active downloads", + "resume": "Resume", + "resumeAll": "Resume All", + "resumeAllTooltip": "Resume all paused downloads", + "restartFailed": "Restart failed", + "restartFile": "Restart file", + "restartRequired": "Restart required", + "resumeRefused": "Resume refused by server. Restart required.", + "addModel": "Add Model", + "addModels": "Add Models", + "advanced": "Advanced", + "allModels": "All Models", + "alpha": "Alpha", + "availableModels": "Available Models", + "baseModel": "Base Model", + "backendDisconnected": "Backend disconnected", + "cancel": "Cancel", + "cancelAll": "Cancel All", + "cancelAllTooltip": "Cancel all active downloads", + "clipEmbed": "CLIP Embed", + "clipLEmbed": "CLIP-L Embed", + "clipGEmbed": "CLIP-G Embed", + "config": "Config", + "reidentify": "Reidentify", + "reidentifyTooltip": "If a model didn't install correctly (e.g. it has the wrong type or doesn't work), you can try reidentifying it. This will reset any custom settings you may have applied.", + "reidentifySuccess": "Model reidentified successfully", + "reidentifyUnknown": "Unable to identify model", + "reidentifyError": "Error reidentifying model", + "reidentifyModels": "Reidentify Models", + "reidentifyModelsConfirm_one": "Are you sure you want to reidentify {{count}} model? This will re-probe its weights file to determine the correct format and settings.", + "reidentifyModelsConfirm_other": "Are you sure you want to reidentify {{count}} models? This will re-probe their weights files to determine the correct format and settings.", + "reidentifyWarning": "This will reset any custom settings you may have applied to these models.", + "modelsReidentified_one": "Successfully reidentified {{count}} model", + "modelsReidentified_other": "Successfully reidentified {{count}} models", + "modelsReidentifyFailed": "Failed to reidentify models", + "someModelsFailedToReidentify_one": "{{count}} model could not be reidentified", + "someModelsFailedToReidentify_other": "{{count}} models could not be reidentified", + "modelsReidentifiedPartial": "Partially completed", + "someModelsReidentified": "{{succeeded}} reidentified, {{failed}} failed", + "modelsReidentifyError": "Error reidentifying models", + "updatePath": "Update Path", + "updatePathTooltip": "Update the file path for this model if you have moved the model files to a new location.", + "updatePathDescription": "Enter the new path to the model file or directory. Use this if you have manually moved the model files on disk.", + "currentPath": "Current Path", + "newPath": "New Path", + "newPathPlaceholder": "Enter new path...", + "pathUpdated": "Model path updated successfully", + "pathUpdateFailed": "Failed to update model path", + "invalidPathFormat": "Path must be an absolute path (e.g., C:\\Models\\... or /home/user/models/...)", + "convert": "Convert", + "convertingModelBegin": "Converting Model. Please wait.", + "convertToDiffusers": "Convert To Diffusers", + "convertToDiffusersHelpText1": "This model will be converted to the 🧨 Diffusers format.", + "convertToDiffusersHelpText2": "This process will replace your Model Manager entry with the Diffusers version of the same model.", + "convertToDiffusersHelpText3": "Your checkpoint file on disk WILL be deleted if it is in the InvokeAI root folder. If it is in a custom location, then it WILL NOT be deleted.", + "convertToDiffusersHelpText4": "This is a one time process only. It might take around 30s-60s depending on the specifications of your computer.", + "convertToDiffusersHelpText5": "Please make sure you have enough disk space. Models generally vary between 2GB-7GB in size.", + "convertToDiffusersHelpText6": "Do you wish to convert this model?", + "cpuOnly": "CPU Only", + "fp8Storage": "FP8 Storage (Save VRAM)", + "runOnCpu": "Run text encoder model on CPU only", + "noDefaultSettings": "No default settings configured for this model. Visit the Model Manager to add default settings.", + "defaultSettings": "Default Settings", + "defaultSettingsSaved": "Default Settings Saved", + "defaultSettingsOutOfSync": "Some settings do not match the model's defaults:", + "restoreDefaultSettings": "Click to use the model's default settings.", + "usingDefaultSettings": "Using model's default settings", + "delete": "Delete", + "deleteConfig": "Delete Config", + "deleteModel": "Delete Model", + "deleteModels": "Delete Models", + "deleteModelImage": "Delete Model Image", + "deleteMsg1": "Are you sure you want to delete this model from InvokeAI?", + "deleteMsg2": "This WILL delete the model from disk if it is in the InvokeAI root folder. If you are using a custom location, then the model WILL NOT be deleted from disk.", + "description": "Description", + "edit": "Edit", + "fileSize": "File Size", + "filterModels": "Filter models", + "fluxRedux": "FLUX Redux", + "externalImageGenerator": "External Image Generator", + "externalProviders": "External Providers", + "externalSetupTitle": "External Providers Setup", + "externalSetupDescription": "Connect an API key to enable external image generation. External starter models auto-install when a provider is configured.", + "externalInstallDefaults": "Auto-install starter models", + "externalProvidersUnavailable": "External providers are not available in this build.", + "externalSetupFooter": "An API key is required. External providers use remote APIs; usage may incur provider-side costs.", + "externalProviderCardDescription": "Configure {{providerId}} credentials for external image generation.", + "externalApiKey": "API Key", + "externalApiKeyPlaceholder": "Paste your API key", + "externalApiKeyPlaceholderSet": "API key configured", + "externalApiKeyHelper": "Stored in api_keys.yaml in your InvokeAI root directory.", + "externalBaseUrl": "Base URL (optional)", + "externalOverrideBaseUrl": "Override Base URL", + "externalBaseUrlPlaceholder": "https://...", + "externalBaseUrlHelper": "Override the default API base URL if needed.", + "externalResetHelper": "Clear API key and base URL.", + "externalProviderSaveFailed": "Failed to save external provider configuration.", + "externalProviderResetFailed": "Failed to reset external provider configuration.", + "height": "Height", + "huggingFace": "HuggingFace", + "huggingFacePlaceholder": "owner/model-name", + "huggingFaceRepoID": "HuggingFace Repo ID", + "huggingFaceHelper": "If multiple models are found in this repo, you will be prompted to select one to install.", + "hfTokenLabel": "HuggingFace Token (Required for some models)", + "hfTokenHelperText": "A HF token is required to use some models. Click here to create or get your token.", + "hfTokenInvalid": "Invalid or Missing HF Token", + "hfForbidden": "You do not have access to this HF model", + "hfForbiddenErrorMessage": "We recommend visiting the repo. The owner may require acceptance of terms in order to download.", + "urlForbidden": "You do not have access to this model", + "urlForbiddenErrorMessage": "You may need to request permission from the site that is distributing the model.", + "hfTokenInvalidErrorMessage": "Invalid or missing HuggingFace token.", + "hfTokenRequired": "You are trying to download a model that requires a valid HuggingFace Token.", + "hfTokenInvalidErrorMessage2": "Update it in the ", + "hfTokenUnableToVerify": "Unable to Verify HF Token", + "hfTokenUnableToVerifyErrorMessage": "Unable to verify HuggingFace token. This is likely due to a network error. Please try again later.", + "hfTokenSaved": "HF Token Saved", + "hfTokenReset": "HF Token Reset", + "urlUnauthorizedErrorMessage": "You may need to configure an API token to access this model.", + "urlUnauthorizedErrorMessage2": "Learn how here.", + "unidentifiedModelTitle": "Unable to identify model", + "unidentifiedModelMessage": "We were unable to identify the type, base and/or format of the installed model. Try editing the model and selecting the appropriate settings for the model.", + "unidentifiedModelMessage2": "If you don't see the correct settings, or the model doesn't work after changing them, ask for help on or create an issue on .", + "imageEncoderModelId": "Image Encoder Model ID", + "installedModelsCount": "{{installed}} of {{total}} models installed.", + "includesNModels": "Includes {{n}} models and their dependencies.", + "allNModelsInstalled": "All {{count}} models installed", + "nToInstall": "{{count}} to install", + "nAlreadyInstalled": "{{count}} already installed", + "installQueue": "Install Queue", + "inplaceInstall": "In-place install", + "inplaceInstallDesc": "Install models without moving the files. When using the model, it will be loaded from its original location. If disabled, the model files will be moved into the Invoke-managed models directory during installation.", + "install": "Install", + "installAll": "Install All", + "installRepo": "Install Repo", + "installBundle": "Install Bundle", + "installBundleMsg1": "Are you sure you want to install the {{bundleName}} bundle?", + "installBundleMsg2": "This bundle will install the following {{count}} models:", + "ipAdapters": "IP Adapters", + "learnMoreAboutSupportedModels": "Learn more about the models we support", + "load": "Load", + "localOnly": "local only", + "manual": "Manual", + "loraModels": "LoRAs", + "main": "Main", + "metadata": "Metadata", + "missingFiles": "Missing Files", + "missingFilesTooltip": "Model files are missing from disk", + "model": "Model", + "modelConversionFailed": "Model Conversion Failed", + "modelConverted": "Model Converted", + "modelDeleted": "Model Deleted", + "modelDeleteFailed": "Failed to delete model", + "modelFormat": "Model Format", + "modelImageDeleted": "Model Image Deleted", + "modelImageDeleteFailed": "Model Image Delete Failed", + "modelImageUpdated": "Model Image Updated", + "modelImageUpdateFailed": "Model Image Update Failed", + "modelManager": "Model Manager", + "modelName": "Model Name", + "modelSettings": "Model Settings", + "modelSettingsWarning": "These settings tell Invoke what kind of model this is and how to load it. If Invoke didn't detect these correctly when you installed the model, or if the model is classified as Unknown, you may need to edit them manually.", + "modelType": "Model Type", + "modelUpdated": "Model Updated", + "modelUpdateFailed": "Model Update Failed", + "sortByName": "Name", + "sortByBase": "Base", + "sortBySize": "Size", + "sortByDateAdded": "Date Added", + "sortByDateModified": "Date Modified", + "sortByPath": "Path", + "sortByType": "Type", + "sortByFormat": "Format", + "sortDefault": "Default", + "name": "Name", + "externalProvider": "External Provider", + "externalCapabilities": "External Capabilities", + "externalDefaults": "External Defaults", + "providerId": "Provider ID", + "providerModelId": "Provider Model ID", + "supportedModes": "Supported Modes", + "supportsNegativePrompt": "Supports Negative Prompt", + "supportsReferenceImages": "Supports Reference Images", + "supportsSeed": "Supports Seed", + "supportsGuidance": "Supports Guidance", + "maxImagesPerRequest": "Max Images Per Request", + "maxReferenceImages": "Max Reference Images", + "maxImageWidth": "Max Image Width", + "maxImageHeight": "Max Image Height", + "numImages": "Num Images", + "modelPickerFallbackNoModelsInstalled": "No models installed.", + "modelPickerFallbackNoModelsInstalled2": "Visit the Model Manager to install models.", + "modelPickerFallbackNoModelsInstalledNonAdmin": "No models installed. Ask your InvokeAI administrator () to install some models.", + "noModelsInstalledDesc1": "Install models with the", + "noModelsInstalledAskAdmin": "Ask your administrator to install some.", + "noModelSelected": "No Model Selected", + "noMatchingModels": "No matching models", + "noModelsInstalled": "No models installed", + "none": "none", + "path": "Path", + "pathToConfig": "Path To Config", + "predictionType": "Prediction Type", + "prune": "Prune", + "pruneTooltip": "Prune finished imports from queue", + "relatedModels": "Related Models", + "showOnlyRelatedModels": "Related", + "repo_id": "Repo ID", + "repoVariant": "Repo Variant", + "scanFolder": "Scan Folder", + "scanFolderHelper": "The folder will be recursively scanned for models. This can take a few moments for very large folders.", + "scanPlaceholder": "Path to a local folder", + "scanResults": "Scan Results", + "search": "Search", + "selected": "Selected", + "selectModel": "Select Model", + "settings": "Settings", + "simpleModelPlaceholder": "URL or path to a local file or diffusers folder", + "source": "Source", + "sourceUrl": "Source URL", + "sigLip": "SigLIP", + "spandrelImageToImage": "Image to Image (Spandrel)", + "starterBundles": "Starter Bundles", + "starterBundleHelpText": "Easily install all models needed to get started with a base model, including a main model, controlnets, IP adapters, and more. Selecting a bundle will skip any models that you already have installed.", + "starterModels": "Starter Models", + "starterModelsInModelManager": "Starter Models can be found in Model Manager", + "bundleAlreadyInstalled": "Bundle already installed", + "bundleAlreadyInstalledDesc": "All models in the {{bundleName}} bundle are already installed.", + "launchpadTab": "Launchpad", + "launchpad": { + "welcome": "Welcome to Model Management", + "description": "Invoke requires models to be installed to utilize most features of the platform. Choose from manual installation options or explore curated starter models.", + "manualInstall": "Manual Installation", + "urlDescription": "Install models from a URL or local file path. Perfect for specific models you want to add.", + "huggingFaceDescription": "Browse and install models directly from HuggingFace repositories.", + "scanFolderDescription": "Scan a local folder to automatically detect and install models.", + "externalDescription": "Connect a Gemini or OpenAI API key to enable external generation. Usage may incur provider-side costs.", + "recommendedModels": "Recommended Models", + "exploreStarter": "Or browse all available starter models", + "quickStart": "Quick Start Bundles", + "bundleDescription": "Each bundle includes essential models for each model family and curated base models to get started.", + "browseAll": "Or browse all available models:", + "stableDiffusion15": "Stable Diffusion 1.5", + "sdxl": "SDXL", + "fluxDev": "FLUX.1 dev" + }, + "controlLora": "Control LoRA", + "llavaOnevision": "LLaVA OneVision", + "textLLM": "Text LLM", + "syncModels": "Sync Models", + "syncModelsTooltip": "Identify and remove unused model files in the InvokeAI root directory.", + "syncModelsDirectory": "Synchronize Models Directory", + "noOrphanedModels": "The models directory is synchronized. No orphaned files found.", + "orphanedModelsFound": "Orphaned Models Found", + "orphanedModelsDescription": "The following model directories are not referenced in the database and can be safely deleted:", + "foundOrphanedModels": "Found {{count}} orphaned model directory", + "foundOrphanedModels_one": "", + "foundOrphanedModels_other": "Found {{count}} orphaned model directories", + "filesCount": "{{count}} file", + "filesCount_one": "", + "filesCount_other": "{{count}} files", + "deleteSelected": "Delete {{count}} selected", + "deleteSelected_one": "", + "deleteSelected_other": "Delete {{count}} selected", + "deselectAll": "Deselect All", + "orphanedModelsDeleted": "Successfully deleted {{count}} orphaned model", + "orphanedModelsDeleted_one": "", + "orphanedModelsDeleted_other": "Successfully deleted {{count}} orphaned models", + "orphanedModelsDeleteErrors": "Some models could not be deleted", + "orphanedModelsDeleteFailed": "Failed to delete orphaned models", + "errorLoadingOrphanedModels": "Error loading orphaned models. Please try again.", + "textualInversions": "Textual Inversions", + "triggerPhrases": "Trigger Phrases", + "loraTriggerPhrases": "LoRA Trigger Phrases", + "mainModelTriggerPhrases": "Main Model Trigger Phrases", + "queueEmpty": "The install queue is empty.", + "selectAll": "Select All", + "selectModelToView": "Select a model to view its details", + "typePhraseHere": "Type phrase here", + "t5Encoder": "T5 Encoder", + "qwen3Encoder": "Qwen3 Encoder", + "qwenVLEncoder": "Qwen2.5-VL Encoder", + "animaVae": "VAE", + "animaVaePlaceholder": "Select Anima-compatible VAE", + "animaQwen3Encoder": "Qwen3 0.6B Encoder", + "animaQwen3EncoderPlaceholder": "Select Qwen3 0.6B encoder", + "zImageVae": "VAE (optional)", + "zImageVaePlaceholder": "From VAE source model", + "zImageQwen3Encoder": "Qwen3 Encoder (optional)", + "zImageQwen3EncoderPlaceholder": "From Qwen3 source model", + "zImageQwen3Source": "Qwen3 & VAE Source Model", + "zImageQwen3SourcePlaceholder": "Required if VAE/Encoder empty", + "flux2KleinVae": "VAE (optional)", + "flux2KleinVaePlaceholder": "From diffusers model", + "flux2KleinVaeNoModelPlaceholder": "No diffusers model available", + "flux2KleinQwen3Encoder": "Qwen3 Encoder (optional)", + "flux2KleinQwen3EncoderPlaceholder": "From diffusers model", + "flux2KleinQwen3EncoderNoModelPlaceholder": "No diffusers model available", + "qwenImageComponentSource": "VAE/Encoder Source (Diffusers)", + "qwenImageComponentSourcePlaceholder": "GGUF models require this unless a standalone VAE & Encoder is installed", + "qwenImageVae": "VAE", + "qwenImageVaePlaceholder": "From VAE/Encoder Source", + "qwenImageQwenVLEncoder": "Qwen2.5-VL Encoder", + "qwenImageQwenVLEncoderPlaceholder": "From VAE/Encoder Source", + "qwenImageQuantization": "Encoder Quantization", + "qwenImageQuantizationNone": "None (bf16)", + "qwenImageQuantizationInt8": "8-bit (int8)", + "qwenImageQuantizationNf4": "4-bit (nf4)", + "upcastAttention": "Upcast Attention", + "uploadImage": "Upload Image", + "urlOrLocalPath": "URL or Local Path", + "urlOrLocalPathHelper": "URLs should point to a single file. Local paths can point to a single file or folder for a single diffusers model.", + "vae": "VAE", + "vaePrecision": "VAE Precision", + "variant": "Variant", + "width": "Width", + "installingBundle": "Installing Bundle", + "installingModel": "Installing Model", + "installingXModels_one": "Installing {{count}} model", + "installingXModels_other": "Installing {{count}} models", + "skippingXDuplicates_one": ", skipping {{count}} duplicate", + "skippingXDuplicates_other": ", skipping {{count}} duplicates", + "manageModels": "Manage Models", + "exportSettings": "Export Settings", + "importSettings": "Import Settings", + "settingsExported": "Model settings exported", + "settingsImported": "Model settings imported", + "settingsImportedPartial": "Model settings partially imported. Incompatible settings were skipped: {{fields}}", + "settingsImportFailed": "Failed to import model settings", + "settingsImportIncompatible": "The settings file contains no compatible settings for this model type", + "settingsImportInvalidFile": "Invalid settings file" + }, + "models": { + "addLora": "Add LoRA", + "concepts": "Concepts", + "loading": "loading", + "noMatchingLoRAs": "No matching LoRAs", + "noMatchingModels": "No matching Models", + "noModelsAvailable": "No models available", + "lora": "LoRA", + "selectModel": "Select a Model", + "noLoRAsInstalled": "No LoRAs installed", + "noRefinerModelsInstalled": "No SDXL Refiner models installed", + "defaultVAE": "Default VAE", + "noCompatibleLoRAs": "No Compatible LoRAs" + }, + "nodes": { + "arithmeticSequence": "Arithmetic Sequence", + "linearDistribution": "Linear Distribution", + "uniformRandomDistribution": "Uniform Random Distribution", + "parseString": "Parse String", + "splitOn": "Split On", + "noBatchGroup": "no group", + "generatorImagesCategory": "Category", + "generatorImages_one": "{{count}} image", + "generatorImages_other": "{{count}} images", + "generatorNRandomValues_one": "{{count}} random value", + "generatorNRandomValues_other": "{{count}} random values", + "generatorNoValues": "empty", + "generatorLoading": "loading", + "generatorLoadFromFile": "Load from File", + "generatorImagesFromBoard": "Images from Board", + "dynamicPromptsRandom": "Dynamic Prompts (Random)", + "dynamicPromptsCombinatorial": "Dynamic Prompts (Combinatorial)", + "addNode": "Add Node", + "addNodeToolTip": "Add Node (Shift+A, Space)", + "addLinearView": "Add to Linear View", + "animatedEdges": "Animated Edges", + "animatedEdgesHelp": "Animate selected edges and edges connected to selected nodes", + "boolean": "Booleans", + "cannotConnectInputToInput": "Cannot connect input to input", + "cannotConnectOutputToOutput": "Cannot connect output to output", + "cannotConnectToSelf": "Cannot connect to self", + "cannotDuplicateConnection": "Cannot create duplicate connections", + "cannotMixAndMatchCollectionItemTypes": "Cannot mix and match collection item types", + "missingNode": "Missing invocation node", + "missingInvocationTemplate": "Missing invocation template", + "missingFieldTemplate": "Missing field template", + "missingSourceOrTargetNode": "Missing source or target node", + "missingSourceOrTargetHandle": "Missing source or target handle", + "nodePack": "Node pack", + "collection": "Collection", + "singleFieldType": "{{name}} (Single)", + "collectionFieldType": "{{name}} (Collection)", + "collectionOrScalarFieldType": "{{name}} (Single or Collection)", + "colorCodeEdges": "Color-Code Edges", + "colorCodeEdgesHelp": "Color-code edges according to their connected fields", + "connectionWouldCreateCycle": "Connection would create a cycle", + "currentImage": "Current Image", + "currentImageDescription": "Displays the current image in the Node Editor", + "downloadWorkflow": "Download Workflow JSON", + "downloadWorkflowError": "Error downloading workflow", + "edge": "Edge", + "edit": "Edit", + "editMode": "Edit in Workflow Editor", + "enum": "Enum", + "executionStateCompleted": "Completed", + "executionStateError": "Error", + "executionStateInProgress": "In Progress", + "fieldTypesMustMatch": "Field types must match", + "fitViewportNodes": "Fit View", + "float": "Float", + "fullyContainNodes": "Fully Contain Nodes to Select", + "fullyContainNodesHelp": "Nodes must be fully inside the selection box to be selected", + "showEdgeLabels": "Show Edge Labels", + "showEdgeLabelsHelp": "Show labels on edges, indicating the connected nodes", + "groupNodesByCategory": "Group Nodes by Category", + "groupNodesByCategoryHelp": "Group nodes by category in the add node dialog", + "hideLegendNodes": "Hide Field Type Legend", + "hideMinimapnodes": "Hide MiniMap", + "inputMayOnlyHaveOneConnection": "Input may only have one connection", + "integer": "Integer", + "ipAdapter": "IP-Adapter", + "loadingNodes": "Loading Nodes...", + "loadWorkflow": "Load Workflow", + "noWorkflows": "No Workflows", + "noMatchingWorkflows": "No Matching Workflows", + "noWorkflow": "No Workflow", + "noWorkflowToSave": "No workflow to save", + "unableToUpdateNode": "Node update failed: node {{node}} of type {{type}} (may require deleting and recreating)", + "mismatchedVersion": "Invalid node: node {{node}} of type {{type}} has mismatched version (try updating?)", + "missingTemplate": "Invalid node: node {{node}} of type {{type}} missing template (not installed?)", + "sourceNodeDoesNotExist": "Invalid edge: source/output node {{node}} does not exist", + "targetNodeDoesNotExist": "Invalid edge: target/input node {{node}} does not exist", + "sourceNodeFieldDoesNotExist": "Invalid edge: source/output field {{node}}.{{field}} does not exist", + "targetNodeFieldDoesNotExist": "Invalid edge: target/input field {{node}}.{{field}} does not exist", + "deletedInvalidEdge": "Deleted invalid edge {{source}} -> {{target}}", + "deletedMissingNodeFieldFormElement": "Deleted missing form field: node {{nodeId}} field {{fieldName}}", + "noConnectionInProgress": "No connection in progress", + "node": "Node", + "nodeOutputs": "Node Outputs", + "nodeSearch": "Search for nodes", + "nodeTemplate": "Node Template", + "nodeType": "Node Type", + "nodeName": "Node Name", + "noFieldsLinearview": "No fields added to Linear View", + "noFieldsViewMode": "This workflow has no selected fields to display. View the full workflow to configure values.", + "workflowHelpText": "Need Help? Check out our guide to Getting Started with Workflows.", + "noNodeSelected": "No node selected", + "nodeOpacity": "Node Opacity", + "nodeVersion": "Node Version", + "noOutputRecorded": "No outputs recorded", + "nodeData": "Node Data", + "notes": "Notes", + "description": "Description", + "notesDescription": "Add notes about your workflow", + "addConnector": "Add Connector", + "deleteConnector": "Delete Connector", + "problemSettingTitle": "Problem Setting Title", + "resetToDefaultValue": "Reset to default value", + "reloadNodeTemplates": "Reload Node Templates", + "removeLinearView": "Remove from Linear View", + "reorderLinearView": "Reorder Linear View", + "newWorkflow": "New Workflow", + "newWorkflowDesc": "Create a new workflow?", + "newWorkflowDesc2": "Your current workflow has unsaved changes.", + "loadWorkflowDesc": "Load workflow?", + "loadWorkflowDesc2": "Your current workflow has unsaved changes.", + "clearWorkflow": "Clear Workflow", + "clearWorkflowDesc": "Clear this workflow and start a new one?", + "clearWorkflowDesc2": "Your current workflow has unsaved changes.", + "scheduler": "Scheduler", + "showLegendNodes": "Show Field Type Legend", + "showMinimapnodes": "Show MiniMap", + "snapToGrid": "Snap to Grid", + "snapToGridHelp": "Snap nodes to grid when moved", + "string": "String", + "unableToLoadWorkflow": "Unable to Load Workflow", + "unableToValidateWorkflow": "Unable to Validate Workflow", + "unknownErrorValidatingWorkflow": "Unknown error validating workflow", + "inputFieldTypeParseError": "Unable to parse type of input field {{node}}.{{field}} ({{message}})", + "outputFieldTypeParseError": "Unable to parse type of output field {{node}}.{{field}} ({{message}})", + "unableToExtractSchemaNameFromRef": "unable to extract schema name from ref", + "unsupportedArrayItemType": "unsupported array item type \"{{type}}\"", + "unsupportedAnyOfLength": "too many union members ({{count}})", + "unsupportedMismatchedUnion": "mismatched CollectionOrScalar type with base types {{firstType}} and {{secondType}}", + "unableToParseFieldType": "unable to parse field type", + "unableToExtractEnumOptions": "unable to extract enum options", + "unknownField": "Unknown field", + "unknownFieldType": "$t(nodes.unknownField) type: {{type}}", + "unknownNode": "Unknown Node", + "unknownNodeType": "Unknown node type", + "unknownTemplate": "Unknown Template", + "unknownInput": "Unknown input: {{name}}", + "missingField_withName": "Missing field \"{{name}}\"", + "unexpectedField_withName": "Unexpected field \"{{name}}\"", + "unknownField_withName": "Unknown field \"{{name}}\"", + "unknownFieldEditWorkflowToFix_withName": "Workflow contains an unknown field \"{{name}}\".\nEdit the workflow to fix the issue.", + "updateNode": "Update Node", + "updateApp": "Update App", + "loadingTemplates": "Loading {{name}}", + "updateAllNodes": "Update Nodes", + "allNodesUpdated": "All Nodes Updated", + "unableToUpdateNodes_one": "Unable to update {{count}} node", + "unableToUpdateNodes_other": "Unable to update {{count}} nodes", + "validateConnections": "Validate Connections and Graph", + "validateConnectionsHelp": "Prevent invalid connections from being made, and invalid graphs from being invoked", + "viewMode": "Use in Linear View", + "unableToGetWorkflowVersion": "Unable to get workflow schema version", + "version": "Version", + "versionUnknown": " Version Unknown", + "workflow": "Workflow", + "graph": "Graph", + "noGraph": "No Graph", + "workflowAuthor": "Author", + "workflowContact": "Contact", + "workflowDescription": "Short Description", + "workflowName": "Name", + "workflowNotes": "Notes", + "workflowSettings": "Workflow Editor Settings", + "workflowTags": "Tags", + "workflowValidation": "Workflow Validation Error", + "workflowVersion": "Version", + "zoomInNodes": "Zoom In", + "zoomOutNodes": "Zoom Out", + "betaDesc": "This invocation is in beta. Until it is stable, it may have breaking changes during app updates. We plan to support this invocation long-term.", + "prototypeDesc": "This invocation is a prototype. It may have breaking changes during app updates and may be removed at any time.", + "internalDesc": "This invocation is used internally by Invoke. It may have breaking changes during app updates and may be removed at any time.", + "specialDesc": "This invocation some special handling in the app. For example, Batch nodes are used to queue multiple graphs from a single workflow.", + "imageAccessError": "Unable to find image {{image_name}}, resetting to default", + "boardAccessError": "Unable to find board {{board_id}}, resetting to default", + "modelAccessError": "Unable to find model {{key}}, resetting to default", + "saveToGallery": "Save To Gallery", + "addItem": "Add Item", + "generateValues": "Generate Values", + "floatRangeGenerator": "Float Range Generator", + "integerRangeGenerator": "Integer Range Generator", + "layout": { + "autoLayout": "Auto Layout", + "layeringStrategy": "Layering Strategy", + "networkSimplex": "Network Simplex", + "longestPath": "Longest Path", + "nodeSpacing": "Node Spacing", + "layerSpacing": "Layer Spacing", + "layoutDirection": "Layout Direction", + "layoutDirectionRight": "Right", + "layoutDirectionDown": "Down", + "alignment": "Node Alignment", + "alignmentUL": "Top Left", + "alignmentDL": "Bottom Left", + "alignmentUR": "Top Right", + "alignmentDR": "Bottom Right" + } + }, + "parameters": { + "aspect": "Aspect", + "duration": "Duration", + "lockAspectRatio": "Lock Aspect Ratio", + "swapDimensions": "Swap Dimensions", + "setToOptimalSize": "Optimize size for model", + "setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (may be too small)", + "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (may be too large)", + "cancel": { + "cancel": "Cancel" + }, + "cfgScale": "CFG Scale", + "cfgRescaleMultiplier": "CFG Rescale Multiplier", + "clipSkip": "CLIP Skip", + "coherenceMode": "Mode", + "coherenceEdgeSize": "Edge Size", + "coherenceMinDenoise": "Min Denoise", + "controlNetControlMode": "Control Mode", + "copyImage": "Copy Image", + "denoisingStrength": "Denoising Strength", + "disabledNoRasterContent": "Disabled (No Raster Content)", + "disabledNotSupported": "Not supported by model", + "downloadImage": "Download Image", + "general": "General", + "guidance": "Guidance", + "height": "Height", + "imageFit": "Fit Initial Image To Output Size", + "images": "Images", + "images_withCount_one": "Image", + "images_withCount_other": "Images", + "infillMethod": "Infill Method", + "infillColorValue": "Fill Color", + "info": "Info", + "invoke": { + "addingImagesTo": "Adding images to", + "boardNotWritable": "You do not have write access to board \"{{boardName}}\". Select a board you own or switch to Uncategorized.", + "modelDisabledForTrial": "Generating with {{modelName}} is not available on trial accounts. Visit your account settings to upgrade.", + "invoke": "Invoke", + "missingFieldTemplate": "Missing field template", + "missingInputForField": "missing input", + "missingNodeTemplate": "Missing node template", + "emptyBatches": "empty batches", + "batchNodeNotConnected": "Batch node not connected: {{label}}", + "batchNodeEmptyCollection": "Some batch nodes have empty collections", + "collectionEmpty": "empty collection", + "collectionTooFewItems": "too few items, minimum {{minItems}}", + "collectionTooManyItems": "too many items, maximum {{maxItems}}", + "collectionStringTooLong": "too long, max {{maxLength}}", + "collectionStringTooShort": "too short, min {{minLength}}", + "collectionNumberGTMax": "{{value}} > {{maximum}} (inc max)", + "collectionNumberLTMin": "{{value}} < {{minimum}} (inc min)", + "collectionNumberGTExclusiveMax": "{{value}} >= {{exclusiveMaximum}} (exc max)", + "collectionNumberLTExclusiveMin": "{{value}} <= {{exclusiveMinimum}} (exc min)", + "collectionNumberNotMultipleOf": "{{value}} not multiple of {{multipleOf}}", + "batchNodeCollectionSizeMismatchNoGroupId": "Batch group collection size mismatch", + "batchNodeCollectionSizeMismatch": "Collection size mismatch on Batch {{batchGroupId}}", + "noModelSelected": "No model selected", + "noStartingFrameImage": "No starting frame image", + "noT5EncoderModelSelected": "No T5 Encoder model selected for FLUX generation", + "noFLUXVAEModelSelected": "No VAE model selected for FLUX generation", + "noCLIPEmbedModelSelected": "No CLIP Embed model selected for FLUX generation", + "noQwen3EncoderModelSelected": "No Qwen3 Encoder model selected for FLUX2 Klein generation", + "noFlux2KleinVaeModelSelected": "No VAE selected. Non-diffusers FLUX.2 Klein models require a standalone VAE", + "noFlux2KleinQwen3EncoderModelSelected": "No Qwen3 Encoder selected. Non-diffusers FLUX.2 Klein models require a standalone Qwen3 Encoder", + "noQwenImageComponentSourceSelected": "GGUF Qwen Image models require a Diffusers Component Source for VAE/encoder", + "noZImageVaeSourceSelected": "No VAE source: Select VAE (FLUX) or Qwen3 Source model", + "noZImageQwen3EncoderSourceSelected": "No Qwen3 Encoder source: Select Qwen3 Encoder or Qwen3 Source model", + "noAnimaVaeModelSelected": "No Anima VAE model selected", + "noAnimaQwen3EncoderModelSelected": "No Anima Qwen3 Encoder model selected", + "fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), bbox width is {{width}}", + "fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), bbox height is {{height}}", + "fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), scaled bbox width is {{width}}", + "fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), scaled bbox height is {{height}}", + "modelIncompatibleBboxWidth": "Bbox width is {{width}} but {{model}} requires multiple of {{multiple}}", + "modelIncompatibleBboxHeight": "Bbox height is {{height}} but {{model}} requires multiple of {{multiple}}", + "modelIncompatibleScaledBboxWidth": "Scaled bbox width is {{width}} but {{model}} requires multiple of {{multiple}}", + "modelIncompatibleScaledBboxHeight": "Scaled bbox height is {{height}} but {{model}} requires multiple of {{multiple}}", + "fluxModelMultipleControlLoRAs": "Can only use 1 Control LoRA at a time", + "incompatibleLoRAs": "Incompatible LoRAs added", + "canvasIsFiltering": "Canvas is busy (filtering)", + "canvasIsTransforming": "Canvas is busy (transforming)", + "canvasIsRasterizing": "Canvas is busy (rasterizing)", + "canvasIsCompositing": "Canvas is busy (compositing)", + "canvasIsSelectingObject": "Canvas is busy (selecting object)", + "noPrompts": "No prompts generated", + "noNodesInGraph": "No nodes in graph", + "systemDisconnected": "System disconnected", + "promptExpansionPending": "Prompt expansion in progress", + "promptExpansionResultPending": "Please accept or discard your prompt expansion result" + }, + "maskBlur": "Mask Blur", + "negativePromptPlaceholder": "Negative Prompt", + "noiseThreshold": "Noise Threshold", + "patchmatchDownScaleSize": "Downscale", + "perlinNoise": "Perlin Noise", + "positivePromptPlaceholder": "Positive Prompt", + "recallMetadata": "Recall Metadata", + "iterations": "Iterations", + "scale": "Scale", + "scaleBeforeProcessing": "Scale Before Processing", + "scaledHeight": "Scaled H", + "scaledWidth": "Scaled W", + "scheduler": "Scheduler", + "dypePreset": "DyPE", + "dypeScale": "DyPE λs", + "dypeExponent": "DyPE λt", + "seamlessXAxis": "Seamless X Axis", + "seamlessYAxis": "Seamless Y Axis", + "colorCompensation": "Color Compensation", + "seed": "Seed", + "seedVarianceEnabled": "Seed Variance Enhancer", + "seedVarianceStrength": "Variance Strength", + "seedVarianceRandomizePercent": "Randomize Percent", + "imageActions": "Image Actions", + "sendToCanvas": "Send To Canvas", + "sendToUpscale": "Send To Upscale", + "showOptionsPanel": "Show Side Panel (O or T)", + "shift": "Shift", + "shuffle": "Shuffle Seed", + "steps": "Steps", + "strength": "Strength", + "symmetry": "Symmetry", + "tileSize": "Tile Size", + "optimizedImageToImage": "Optimized Image-to-Image", + "type": "Type", + "postProcessing": "Post-Processing (Shift + U)", + "processImage": "Process Image", + "upscaling": "Upscaling", + "useAll": "Use All", + "useSize": "Use Size", + "useCpuNoise": "Use CPU Noise", + "remixImage": "Remix Image", + "usePrompt": "Use Prompt", + "useSeed": "Use Seed", + "useClipSkip": "Use CLIP Skip", + "width": "Width", + "gaussianBlur": "Gaussian Blur", + "boxBlur": "Box Blur", + "staged": "Staged", + "resolution": "Resolution", + "imageSize": "Image Size", + "quality": "Quality", + "qualityOptions": { + "auto": "Auto", + "high": "High", + "medium": "Medium", + "low": "Low" + }, + "background": "Background", + "backgroundOptions": { + "auto": "Auto", + "transparent": "Transparent", + "opaque": "Opaque" + }, + "inputFidelity": "Input Fidelity", + "inputFidelityOptions": { + "default": "Default", + "low": "Low", + "high": "High" + }, + "temperature": "Temperature", + "thinkingLevel": "Thinking Level", + "thinkingLevelOptions": { + "default": "Default", + "minimal": "Minimal", + "high": "High" + }, + "watermark": "Watermark", + "optimizePrompt": "Optimize Prompt", + "modelDisabledForTrial": "Generating with {{modelName}} is not available on trial accounts. Visit your account settings to upgrade." + }, + "dynamicPrompts": { + "showDynamicPrompts": "Show Dynamic Prompts", + "dynamicPrompts": "Dynamic Prompts", + "maxPrompts": "Max Prompts", + "promptsPreview": "Prompts Preview", + "seedBehaviour": { + "label": "Seed Behaviour", + "perIterationLabel": "Seed per Iteration", + "perIterationDesc": "Use a different seed for each iteration", + "perPromptLabel": "Seed per Image", + "perPromptDesc": "Use a different seed for each image" + }, + "loading": "Generating Dynamic Prompts...", + "problemGeneratingPrompts": "Problem generating prompts", + "promptsToGenerate": "Prompts to Generate" + }, + "sdxl": { + "cfgScale": "CFG Scale", + "concatPromptStyle": "Linking Prompt & Style", + "freePromptStyle": "Manual Style Prompting", + "denoisingStrength": "Denoising Strength", + "loading": "Loading...", + "negAestheticScore": "Negative Aesthetic Score", + "negStylePrompt": "Negative Style Prompt", + "noModelsAvailable": "No models available", + "posAestheticScore": "Positive Aesthetic Score", + "posStylePrompt": "Positive Style Prompt", + "refiner": "Refiner", + "refinermodel": "Refiner Model", + "refinerStart": "Refiner Start", + "refinerSteps": "Refiner Steps", + "scheduler": "Scheduler", + "steps": "Steps" + }, + "settings": { + "antialiasProgressImages": "Antialias Progress Images", + "beta": "Beta", + "confirmOnDelete": "Confirm On Delete", + "confirmOnNewSession": "Confirm On New Session", + "developer": "Developer", + "displayInProgress": "Display Progress Images", + "enableInformationalPopovers": "Enable Informational Popovers", + "informationalPopoversDisabled": "Informational Popovers Disabled", + "informationalPopoversDisabledDesc": "Informational popovers have been disabled. Enable them in Settings.", + "enableModelDescriptions": "Enable Model Descriptions in Dropdowns", + "enableHighlightFocusedRegions": "Highlight Focused Regions", + "middleClickOpenInNewTab": "Use Middle Click to Open Images in New Tab", + "modelDescriptionsDisabled": "Model Descriptions in Dropdowns Disabled", + "modelDescriptionsDisabledDesc": "Model descriptions in dropdowns have been disabled. Enable them in Settings.", + "enableInvisibleWatermark": "Enable Invisible Watermark", + "enableNSFWChecker": "Enable NSFW Checker", + "general": "General", + "generation": "Generation", + "imageSubfolderStrategy": "Image Subfolder Strategy", + "imageSubfolderStrategyDate": "Date", + "imageSubfolderStrategyFlat": "Flat", + "imageSubfolderStrategyHash": "Hash", + "imageSubfolderStrategySaveFailed": "Failed to save Image Subfolder Strategy", + "imageSubfolderStrategyType": "Type", + "imageSubfolderStrategyUnknown": "Unknown ({{strategy}})", + "maxQueueHistory": "Max Queue History", + "maxQueueHistorySaveFailed": "Failed to save Max Queue History", + "models": "Models", + "preferAttentionStyleNumeric": "Prefer Numeric Attention Style", + "prompt": "Prompt", + "resetComplete": "Web UI has been reset.", + "resetWebUI": "Reset Web UI", + "resetWebUIDesc1": "Resetting the web UI only resets the browser's local cache of your images and remembered settings. It does not delete any images from disk.", + "resetWebUIDesc2": "If images aren't showing up in the gallery or something else isn't working, please try resetting before submitting an issue on GitHub.", + "showDetailedInvocationProgress": "Show Progress Details", + "showProgressInViewer": "Show Progress Images in Viewer", + "ui": "User Interface", + "clearIntermediatesDisabled": "Queue must be empty to clear intermediates", + "clearIntermediatesDesc1": "Clearing intermediates will reset your Canvas and ControlNet state.", + "clearIntermediatesDesc2": "Intermediate images are byproducts of generation, different from the result images in the gallery. Clearing intermediates will free disk space.", + "clearIntermediatesDesc3": "Your gallery images will not be deleted.", + "clearIntermediates": "Clear Intermediates", + "clearIntermediatesWithCount_one": "Clear {{count}} Intermediate", + "clearIntermediatesWithCount_other": "Clear {{count}} Intermediates", + "intermediatesCleared_one": "Cleared {{count}} Intermediate", + "intermediatesCleared_other": "Cleared {{count}} Intermediates", + "intermediatesClearedFailed": "Problem Clearing Intermediates", + "reloadingIn": "Reloading in", + "externalProviders": "External Providers", + "externalProviderConfigured": "Configured", + "externalProviderNotConfigured": "API Key Required", + "externalProviderNotConfiguredHint": "Add your API key in Model Manager or the server config to enable this provider." + }, + "toast": { + "addedToBoard": "Added to board {{name}}'s assets", + "addedToUncategorized": "Added to board $t(boards.uncategorized)'s assets", + "baseModelChanged": "Base Model Changed", + "modelDownloadPaused": "Model download paused", + "modelDownloadResumed": "Resuming download", + "modelDownloadRestartFailed": "Restart failed downloads", + "modelDownloadRestartFile": "Restarting file download", + "modelDownloadRestartedFromScratch": "Partial file missing. Restarted download from the beginning.", + "baseModelChangedCleared_one": "Updated, cleared or disabled {{count}} incompatible submodel", + "baseModelChangedCleared_other": "Updated, cleared or disabled {{count}} incompatible submodels", + "kleinEncoderCleared": "Qwen3 Encoder Cleared", + "kleinEncoderClearedDescription": "Please select a compatible Qwen3 encoder for the new Klein model variant", + "schedulerReset": "Scheduler Reset", + "schedulerResetZImageBase": "LCM scheduler is not compatible with Z-Image Base models. Reset to Euler.", + "canceled": "Processing Canceled", + "connected": "Connected to Server", + "imageCopied": "Image Copied", + "linkCopied": "Link Copied", + "unableToLoadImage": "Unable to Load Image", + "unableToLoadImageMetadata": "Unable to Load Image Metadata", + "unableToLoadStylePreset": "Unable to Load Style Preset", + "stylePresetLoaded": "Style Preset Loaded", + "imageNotLoadedDesc": "Could not find image", + "imageSaved": "Image Saved", + "imageSavingFailed": "Image Saving Failed", + "imageUploaded": "Image Uploaded", + "imageUploadFailed": "Image Upload Failed", + "importFailed": "Import Failed", + "importSuccessful": "Import Successful", + "invalidUpload": "Invalid Upload", + "layerCopiedToClipboard": "Layer Copied to Clipboard", + "layerSavedToAssets": "Layer Saved to Assets", + "loadedWithWarnings": "Workflow Loaded with Warnings", + "modelAddedSimple": "Model Added to Queue", + "modelImportCanceled": "Model Import Canceled", + "outOfMemoryError": "Out of Memory Error", + "outOfMemoryErrorDescLocal": "Follow our Low VRAM guide to reduce OOMs.", + "outOfMemoryErrorDesc": "Your current generation settings exceed system capacity. Please adjust your settings and try again.", + "parameters": "Parameters", + "parameterSet": "Parameter Recalled", + "parameterSetDesc": "Recalled {{parameter}}", + "parameterNotSet": "Parameter Not Recalled", + "parameterNotSetDesc": "Unable to recall {{parameter}}", + "parameterNotSetDescWithMessage": "Unable to recall {{parameter}}: {{message}}", + "parametersSet": "Parameters Recalled", + "parametersNotSet": "Parameters Not Recalled", + "errorCopied": "Error Copied", + "problemCopyingImage": "Unable to Copy Image", + "problemCopyingLayer": "Unable to Copy Layer", + "problemSavingLayer": "Unable to Save Layer", + "problemDownloadingImage": "Unable to Download Image", + "noRasterLayers": "No Raster Layers Found", + "noRasterLayersDesc": "Create at least one raster layer to export to PSD", + "noActiveRasterLayers": "No Active Raster Layers", + "noActiveRasterLayersDesc": "Enable at least one raster layer to export to PSD", + "noVisibleRasterLayers": "No Visible Raster Layers", + "noVisibleRasterLayersDesc": "Enable at least one raster layer to export to PSD", + "invalidCanvasDimensions": "Invalid Canvas Dimensions", + "canvasTooLarge": "Canvas Too Large", + "canvasTooLargeDesc": "Canvas dimensions exceed the maximum allowed size for PSD export. Reduce the total width and height of the canvas of the canvas and try again.", + "failedToProcessLayers": "Failed to Process Layers", + "psdExportSuccess": "PSD Export Complete", + "psdExportSuccessDesc": "Successfully exported {{count}} layers to PSD file", + "problemExportingPSD": "Problem Exporting PSD", + "canvasManagerNotAvailable": "Canvas Manager Not Available", + "noValidLayerAdapters": "No Valid Layer Adapters Found", + "pasteSuccess": "Pasted to {{destination}}", + "pasteFailed": "Paste Failed", + "prunedQueue": "Pruned Queue", + "sentToCanvas": "Sent to Canvas", + "sentToUpscale": "Sent to Upscale", + "serverError": "Server Error", + "sessionRef": "Session: {{sessionId}}", + "setControlImage": "Set as control image", + "setNodeField": "Set as node field", + "somethingWentWrong": "Something Went Wrong", + "uploadFailed": "Upload failed", + "imagesWillBeAddedTo": "Uploaded images will be added to board {{boardName}}'s assets.", + "uploadFailedInvalidUploadDesc_withCount_one": "Must be maximum of 1 PNG, JPEG or WEBP image.", + "uploadFailedInvalidUploadDesc_withCount_other": "Must be maximum of {{count}} PNG, JPEG or WEBP images.", + "uploadFailedInvalidUploadDesc": "Must be PNG, JPEG or WEBP images.", + "workflowLoaded": "Workflow Loaded", + "problemRetrievingWorkflow": "Problem Retrieving Workflow", + "workflowDeleted": "Workflow Deleted", + "problemDeletingWorkflow": "Problem Deleting Workflow", + "unableToCopy": "Unable to Copy", + "unableToCopyDesc": "Your browser does not support clipboard access. Firefox users may be able to fix this by following ", + "unableToCopyDesc_theseSteps": "these steps", + "fluxFillIncompatibleWithT2IAndI2I": "FLUX Fill is not compatible with Text to Image or Image to Image. Use other FLUX models for these tasks.", + "imagenIncompatibleGenerationMode": "Google {{model}} supports Text to Image only. Use other models for Image to Image, Inpainting and Outpainting tasks.", + "chatGPT4oIncompatibleGenerationMode": "ChatGPT 4o supports Text to Image and Image to Image only. Use other models Inpainting and Outpainting tasks.", + "fluxKontextIncompatibleGenerationMode": "FLUX Kontext does not support generation from images placed on the canvas. Re-try using the Reference Image section and disable any Raster Layers.", + "problemUnpublishingWorkflow": "Problem Unpublishing Workflow", + "problemUnpublishingWorkflowDescription": "There was a problem unpublishing the workflow. Please try again.", + "workflowUnpublished": "Workflow Unpublished", + "promptGenerationStarted": "Prompt generation started", + "uploadAndPromptGenerationFailed": "Failed to upload image and generate prompt", + "promptExpansionFailed": "We ran into an issue. Please try prompt expansion again.", + "maskInverted": "Mask Inverted", + "maskInvertFailed": "Failed to Invert Mask", + "noVisibleMasks": "No Visible Masks", + "noVisibleMasksDesc": "Create or enable at least one inpaint mask to invert", + "noInpaintMaskSelected": "No Inpaint Mask Selected", + "noInpaintMaskSelectedDesc": "Select an inpaint mask to invert", + "invalidBbox": "Invalid Bounding Box", + "invalidBboxDesc": "The bounding box has no valid dimensions" + }, + "popovers": { + "clipSkip": { + "heading": "CLIP Skip", + "paragraphs": [ + "How many layers of the CLIP model to skip.", + "Certain models are better suited to be used with CLIP Skip." + ] + }, + "paramNegativeConditioning": { + "heading": "Negative Prompt", + "paragraphs": [ + "The generation process avoids the concepts in the negative prompt. Use this to exclude qualities or objects from the output.", + "Supports Compel syntax and embeddings." + ] + }, + "paramPositiveConditioning": { + "heading": "Positive Prompt", + "paragraphs": [ + "Guides the generation process. You may use any words or phrases.", + "Compel and Dynamic Prompts syntaxes and embeddings." + ] + }, + "paramScheduler": { + "heading": "Scheduler", + "paragraphs": [ + "Scheduler used during the generation process.", + "Each scheduler defines how to iteratively add noise to an image or how to update a sample based on a model's output." + ] + }, + "fluxDypePreset": { + "heading": "DyPE (High-Resolution)", + "paragraphs": [ + "Dynamic Position Extrapolation (DyPE) improves FLUX generation quality at resolutions above the training size (1024px).", + "Off: Standard generation. Auto: Automatically enables for images > 1536px. 4K: Optimized settings for 4K resolution output." + ] + }, + "fluxDypeScale": { + "heading": "DyPE Scale (λs)", + "paragraphs": [ + "Controls the magnitude of the DyPE modulation. Higher values = stronger extrapolation.", + "Default: 2.0. Range: 0.0-8.0." + ] + }, + "fluxDypeExponent": { + "heading": "DyPE Exponent (λt)", + "paragraphs": [ + "Controls the strength of the dynamic effect over time.", + "2.0: Recommended for 4K+ resolutions. Aggressive schedule that transitions quickly to clean up artifacts.", + "1.0: Good starting point for ~2K-3K resolutions.", + "0.5: Gentler schedule for resolutions just above native (1024px)." + ] + }, + "seedVarianceEnhancer": { + "heading": "Seed Variance Enhancer", + "paragraphs": [ + "Z-Image-Turbo can produce relatively similar images with different seeds. This feature adds seed-based noise to the text embeddings to increase visual variation while maintaining reproducibility.", + "Enable this to get more diverse results when exploring different seeds." + ] + }, + "seedVarianceStrength": { + "heading": "Variance Strength", + "paragraphs": [ + "Controls the intensity of the noise added to embeddings. The strength is automatically calibrated relative to the embedding's standard deviation.", + "Values less than 0.1 will produce subtle variations, increasing to stronger ones at 0.5. Values above 0.5 may lead to unexpected results." + ] + }, + "seedVarianceRandomizePercent": { + "heading": "Randomize Percent", + "paragraphs": [ + "Percentage of embedding values that receive noise (1-100%).", + "Lower values create more selective noise patterns, while 100% affects all values equally." + ] + }, + "compositingMaskBlur": { + "heading": "Mask Blur", + "paragraphs": ["The blur radius of the mask."] + }, + "compositingBlurMethod": { + "heading": "Blur Method", + "paragraphs": ["The method of blur applied to the masked area."] + }, + "compositingCoherencePass": { + "heading": "Coherence Pass", + "paragraphs": ["A second round of denoising helps to composite the Inpainted/Outpainted image."] + }, + "compositingCoherenceMode": { + "heading": "Mode", + "paragraphs": ["Method used to create a coherent image with the newly generated masked area."] + }, + "compositingCoherenceEdgeSize": { + "heading": "Edge Size", + "paragraphs": ["The edge size of the coherence pass."] + }, + "compositingCoherenceMinDenoise": { + "heading": "Minimum Denoise", + "paragraphs": [ + "Minimum denoise strength for the Coherence mode", + "The minimum denoise strength for the coherence region when inpainting or outpainting" + ] + }, + "compositingMaskAdjustments": { + "heading": "Mask Adjustments", + "paragraphs": ["Adjust the mask."] + }, + "inpainting": { + "heading": "Inpainting", + "paragraphs": ["Controls which area is modified, guided by Denoising Strength."] + }, + "rasterLayer": { + "heading": "Raster Layer", + "paragraphs": ["Pixel-based content of your canvas, used during image generation."] + }, + "regionalGuidance": { + "heading": "Regional Guidance", + "paragraphs": ["Brush to guide where elements from global prompts should appear."] + }, + "regionalGuidanceAndReferenceImage": { + "heading": "Regional Guidance and Regional Reference Image", + "paragraphs": [ + "For Regional Guidance, brush to guide where elements from global prompts should appear.", + "For Regional Reference Image, brush to apply a reference image to specific areas." + ] + }, + "globalReferenceImage": { + "heading": "Global Reference Image", + "paragraphs": ["Applies a reference image to influence the entire generation."] + }, + "regionalReferenceImage": { + "heading": "Regional Reference Image", + "paragraphs": ["Brush to apply a reference image to specific areas."] + }, + "controlNet": { + "heading": "ControlNet", + "paragraphs": [ + "ControlNets provide guidance to the generation process, helping create images with controlled composition, structure, or style, depending on the model selected." + ] + }, + "controlNetBeginEnd": { + "heading": "Begin / End Step Percentage", + "paragraphs": [ + "This setting determines which portion of the denoising (generation) process incorporates the guidance from this layer.", + "• Start Step (%): Specifies when to begin applying the guidance from this layer during the generation process.", + "• End Step (%): Specifies when to stop applying this layer's guidance and revert general guidance from the model and other settings." + ] + }, + "controlNetControlMode": { + "heading": "Control Mode", + "paragraphs": ["Lend more weight to either the prompt or ControlNet."] + }, + "controlNetProcessor": { + "heading": "Processor", + "paragraphs": [ + "Method of processing the input image to guide the generation process. Different processors will provide different effects or styles in your generated images." + ] + }, + "controlNetResizeMode": { + "heading": "Resize Mode", + "paragraphs": ["Method to fit Control Adapter's input image size to the output generation size."] + }, + "ipAdapterMethod": { + "heading": "Mode", + "paragraphs": ["The mode defines how the reference image will guide the generation process."] + }, + "controlNetWeight": { + "heading": "Weight", + "paragraphs": [ + "Adjusts how strongly the layer influences the generation process", + "• Higher Weight (.75-2): Creates a more significant impact on the final result.", + "• Lower Weight (0-.75): Creates a smaller impact on the final result." + ] + }, + "dynamicPrompts": { + "heading": "Dynamic Prompts", + "paragraphs": [ + "Dynamic Prompts parses a single prompt into many.", + "The basic syntax is \"a {red|green|blue} ball\". This will produce three prompts: \"a red ball\", \"a green ball\" and \"a blue ball\".", + "You can use the syntax as many times as you like in a single prompt, but be sure to keep the number of prompts generated in check with the Max Prompts setting." + ] + }, + "dynamicPromptsMaxPrompts": { + "heading": "Max Prompts", + "paragraphs": ["Limits the number of prompts that can be generated by Dynamic Prompts."] + }, + "dynamicPromptsSeedBehaviour": { + "heading": "Seed Behaviour", + "paragraphs": [ + "Controls how the seed is used when generating prompts.", + "Per Iteration will use a unique seed for each iteration. Use this to explore prompt variations on a single seed.", + "For example, if you have 5 prompts, each image will use the same seed.", + "Per Image will use a unique seed for each image. This provides more variation." + ] + }, + "imageFit": { + "heading": "Fit Initial Image to Output Size", + "paragraphs": [ + "Resizes the initial image to the width and height of the output image. Recommended to enable." + ] + }, + "infillMethod": { + "heading": "Infill Method", + "paragraphs": ["Method of infilling during the Outpainting or Inpainting process."] + }, + "lora": { + "heading": "LoRA", + "paragraphs": ["Lightweight models that are used in conjunction with base models."] + }, + "loraWeight": { + "heading": "Weight", + "paragraphs": ["Weight of the LoRA. Higher weight will lead to larger impacts on the final image."] + }, + "noiseUseCPU": { + "heading": "Use CPU Noise", + "paragraphs": [ + "Controls whether noise is generated on the CPU or GPU.", + "With CPU Noise enabled, a particular seed will produce the same image on any machine.", + "There is no performance impact to enabling CPU Noise." + ] + }, + "paramAspect": { + "heading": "Aspect", + "paragraphs": [ + "Aspect ratio of the generated image. Changing the ratio will update the Width and Height accordingly.", + "\"Optimize\" will set the Width and Height to optimal dimensions for the chosen model." + ] + }, + "paramCFGScale": { + "heading": "CFG Scale", + "paragraphs": [ + "Controls how much the prompt influences the generation process.", + "High CFG Scale values can result in over-saturation and distorted generation results. " + ] + }, + "paramGuidance": { + "heading": "Guidance", + "paragraphs": [ + "Controls how much the prompt influences the generation process.", + "High guidance values can result in over-saturation and high or low guidance may result in distorted generation results. Guidance only applies to FLUX DEV models." + ] + }, + "paramCFGRescaleMultiplier": { + "heading": "CFG Rescale Multiplier", + "paragraphs": [ + "Rescale multiplier for CFG guidance, used for models trained using zero-terminal SNR (ztsnr).", + "Suggested value of 0.7 for these models." + ] + }, + "paramDenoisingStrength": { + "heading": "Denoising Strength", + "paragraphs": [ + "Controls how much the generated image varies from the raster layers.", + "Lower strength stays closer to the combined visible raster layers. Higher strength relies more on the global prompt.", + "When there are no raster layers with visible content, this setting is ignored." + ] + }, + "paramHeight": { + "heading": "Height", + "paragraphs": ["Height of the generated image. Must be a multiple of 8."] + }, + "paramHrf": { + "heading": "Enable High Resolution Fix", + "paragraphs": [ + "Generate high quality images at a larger resolution than optimal for the model. Generally used to prevent duplication in the generated image." + ] + }, + "paramIterations": { + "heading": "Iterations", + "paragraphs": [ + "The number of images to generate.", + "If Dynamic Prompts is enabled, each of the prompts will be generated this many times." + ] + }, + "paramModel": { + "heading": "Model", + "paragraphs": [ + "Model used for generation. Different models are trained to specialize in producing different aesthetic results and content." + ] + }, + "paramRatio": { + "heading": "Aspect Ratio", + "paragraphs": [ + "The aspect ratio of the dimensions of the image generated.", + "An image size (in number of pixels) equivalent to 512x512 is recommended for SD1.5 models and a size equivalent to 1024x1024 is recommended for SDXL models." + ] + }, + "paramSeed": { + "heading": "Seed", + "paragraphs": [ + "Controls the starting noise used for generation.", + "Disable the \"Random\" option to produce identical results with the same generation settings." + ] + }, + "paramSteps": { + "heading": "Steps", + "paragraphs": [ + "Number of steps that will be performed in each generation.", + "Higher step counts will typically create better images but will require more generation time." + ] + }, + "paramUpscaleMethod": { + "heading": "Upscale Method", + "paragraphs": ["Method used to upscale the image for High Resolution Fix."] + }, + "paramVAE": { + "heading": "VAE", + "paragraphs": ["Model used for translating AI output into the final image."] + }, + "paramVAEPrecision": { + "heading": "VAE Precision", + "paragraphs": [ + "The precision used during VAE encoding and decoding.", + "Fp16/Half precision is more efficient, at the expense of minor image variations." + ] + }, + "paramWidth": { + "heading": "Width", + "paragraphs": ["Width of the generated image. Must be a multiple of 8."] + }, + "patchmatchDownScaleSize": { + "heading": "Downscale", + "paragraphs": [ + "How much downscaling occurs before infilling.", + "Higher downscaling will improve performance and reduce quality." + ] + }, + "refinerModel": { + "heading": "Refiner Model", + "paragraphs": [ + "Model used during the refiner portion of the generation process.", + "Similar to the Generation Model." + ] + }, + "refinerPositiveAestheticScore": { + "heading": "Positive Aesthetic Score", + "paragraphs": [ + "Weight generations to be more similar to images with a high aesthetic score, based on the training data." + ] + }, + "refinerNegativeAestheticScore": { + "heading": "Negative Aesthetic Score", + "paragraphs": [ + "Weight generations to be more similar to images with a low aesthetic score, based on the training data." + ] + }, + "refinerScheduler": { + "heading": "Scheduler", + "paragraphs": [ + "Scheduler used during the refiner portion of the generation process.", + "Similar to the Generation Scheduler." + ] + }, + "refinerStart": { + "heading": "Refiner Start", + "paragraphs": [ + "Where in the generation process the refiner will start to be used.", + "0 means the refiner will be used for the entire generation process, 0.8 means the refiner will be used for the last 20% of the generation process." + ] + }, + "refinerSteps": { + "heading": "Steps", + "paragraphs": [ + "Number of steps that will be performed during the refiner portion of the generation process.", + "Similar to the Generation Steps." + ] + }, + "refinerCfgScale": { + "heading": "CFG Scale", + "paragraphs": [ + "Controls how much the prompt influences the generation process.", + "Similar to the Generation CFG Scale." + ] + }, + "scaleBeforeProcessing": { + "heading": "Scale Before Processing", + "paragraphs": [ + "\"Auto\" scales the selected area to the size best suited for the model before the image generation process.", + "\"Manual\" allows you to choose the width and height the selected area will be scaled to before the image generation process." + ] + }, + "seamlessTilingXAxis": { + "heading": "Seamless Tiling X Axis", + "paragraphs": ["Seamlessly tile an image along the horizontal axis."] + }, + "seamlessTilingYAxis": { + "heading": "Seamless Tiling Y Axis", + "paragraphs": ["Seamlessly tile an image along the vertical axis."] + }, + "colorCompensation": { + "heading": "Color Compensation", + "paragraphs": ["Adjust the input image to reduce color shifts during inpainting or img2img (SDXL Only)."] + }, + "upscaleModel": { + "heading": "Upscale Model", + "paragraphs": [ + "The upscale model scales the image to the output size before details are added. Any supported upscale model may be used, but some are specialized for different kinds of images, like photos or line drawings." + ] + }, + "scale": { + "heading": "Scale", + "paragraphs": [ + "Scale controls the output image size, and is based on a multiple of the input image resolution. For example a 2x upscale on a 1024x1024 image would produce a 2048 x 2048 output." + ] + }, + "creativity": { + "heading": "Creativity", + "paragraphs": [ + "Creativity controls the amount of freedom granted to the model when adding details. Low creativity stays close to the original image, while high creativity allows for more change. When using a prompt, high creativity increases the influence of the prompt." + ] + }, + "structure": { + "heading": "Structure", + "paragraphs": [ + "Structure controls how closely the output image will keep to the layout of the original. Low structure allows major changes, while high structure strictly maintains the original composition and layout." + ] + }, + "tileSize": { + "heading": "Tile Size", + "paragraphs": [ + "Controls the size of tiles used during the upscaling process. Larger tiles use more memory but may produce better results.", + "SD1.5 models default to 768, while SDXL models default to 1024. Reduce tile size if you encounter memory issues." + ] + }, + "tileOverlap": { + "heading": "Tile Overlap", + "paragraphs": [ + "Controls the overlap between adjacent tiles during upscaling. Higher overlap values help reduce visible seams between tiles but use more memory.", + "The default value of 128 works well for most cases, but you can adjust based on your specific needs and memory constraints." + ] + }, + "fluxDevLicense": { + "heading": "Non-Commercial License", + "paragraphs": [ + "This model is licensed for non-commercial use only. FLUX.1 [dev] models use the FLUX.1 [dev] Non-Commercial License, and FLUX.2 Klein 9B uses the FLUX.2 Non-Commercial License." + ] + }, + "optimizedDenoising": { + "heading": "Optimized Image-to-Image", + "paragraphs": [ + "Enable 'Optimized Image-to-Image' for a more gradual Denoise Strength scale for image-to-image and inpainting transformations with Flux models. This setting improves the ability to control the amount of change applied to an image, but may be turned off if you prefer to use the standard Denoise Strength scale. This setting is still being tuned and is in beta status." + ] + }, + "cpuOnly": { + "heading": "CPU Only", + "paragraphs": [ + "When enabled, only the text encoder component will run on CPU instead of GPU.", + "This saves VRAM for the denoiser while only slightly impacting performance. The conditioning outputs are automatically moved to GPU for the denoiser." + ] + }, + "fp8Storage": { + "heading": "FP8 Storage", + "paragraphs": [ + "Stores model weights in FP8 format in VRAM, reducing memory usage by approximately 50% compared to FP16.", + "During inference, weights are cast layer-by-layer to the compute precision (FP16/BF16), so image quality is preserved. Works on all CUDA GPUs." + ] + } + }, + "workflows": { + "chooseWorkflowFromLibrary": "Choose Workflow from Library", + "defaultWorkflows": "Default Workflows", + "userWorkflows": "User Workflows", + "projectWorkflows": "Project Workflows", + "ascending": "Ascending", + "created": "Created", + "descending": "Descending", + "workflows": "Workflows", + "workflowLibrary": "Workflow Library", + "loadMore": "Load More", + "allLoaded": "All Workflows Loaded", + "searchPlaceholder": "Search by name, description or tags", + "filterByTags": "Filter by Tags", + "tags": "Tags", + "yourWorkflows": "Your Workflows", + "recentlyOpened": "Recently Opened", + "sharedWorkflows": "Shared Workflows", + "shareWorkflow": "Shared workflow", + "noRecentWorkflows": "No Recent Workflows", + "private": "Private", + "shared": "Shared", + "published": "Published", + "browseWorkflows": "Browse Workflows", + "deselectAll": "Deselect All", + "recommended": "Recommended For You", + "opened": "Opened", + "openWorkflow": "Open Workflow", + "updated": "Updated", + "uploadWorkflow": "Load from File", + "deleteWorkflow": "Delete Workflow", + "deleteWorkflow2": "Are you sure you want to delete this workflow? This cannot be undone.", + "unnamedWorkflow": "Unnamed Workflow", + "downloadWorkflow": "Save to File", + "saveWorkflow": "Save Workflow", + "saveWorkflowAs": "Save Workflow As", + "saveWorkflowToProject": "Save Workflow to Project", + "savingWorkflow": "Saving Workflow...", + "problemSavingWorkflow": "Problem Saving Workflow", + "workflowSaved": "Workflow Saved", + "name": "Name", + "noWorkflows": "No Workflows", + "problemLoading": "Problem Loading Workflows", + "loading": "Loading Workflows", + "noDescription": "No description", + "searchWorkflows": "Search Workflows", + "clearWorkflowSearchFilter": "Clear Workflow Search Filter", + "workflowName": "Workflow Name", + "newWorkflowCreated": "New Workflow Created", + "workflowCleared": "Workflow Cleared", + "workflowEditorMenu": "Workflow Editor Menu", + "loadFromGraph": "Load Workflow from Graph", + "convertGraph": "Convert Graph", + "loadWorkflow": "$t(common.load) Workflow", + "autoLayout": "Auto Layout", + "edit": "Edit", + "view": "View", + "download": "Download", + "copyShareLink": "Copy Share Link", + "copyShareLinkForWorkflow": "Copy Share Link for Workflow", + "delete": "Delete", + "openLibrary": "Open Library", + "workflowThumbnail": "Workflow Thumbnail", + "saveChanges": "Save Changes", + "emptyStringPlaceholder": "", + "builder": { + "deleteAllElements": "Delete All Form Elements", + "resetAllNodeFields": "Reset All Node Fields", + "builder": "Form Builder", + "layout": "Layout", + "row": "Row", + "column": "Column", + "container": "Container", + "containerRowLayout": "Container (row layout)", + "containerColumnLayout": "Container (column layout)", + "heading": "Heading", + "text": "Text", + "divider": "Divider", + "nodeField": "Node Field", + "zoomToNode": "Zoom to Node", + "nodeFieldTooltip": "To add a node field, click the small plus sign button on the field in the Workflow Editor, or drag the field by its name into the form.", + "addToForm": "Add to Form", + "removeFromForm": "Remove from Form", + "label": "Label", + "showDescription": "Show Description", + "showShuffle": "Show Shuffle", + "shuffle": "Shuffle", + "component": "Component", + "numberInput": "Number Input", + "singleLine": "Single Line", + "multiLine": "Multi Line", + "slider": "Slider", + "dropdown": "Dropdown", + "addOption": "Add Option", + "resetOptions": "Reset Options", + "both": "Both", + "emptyRootPlaceholderViewMode": "Click Edit to start building a form for this workflow.", + "emptyRootPlaceholderEditMode": "Drag a form element or node field here to get started.", + "containerPlaceholder": "Empty Container", + "headingPlaceholder": "Empty Heading", + "textPlaceholder": "Empty Text", + "workflowBuilderAlphaWarning": "The workflow builder is currently in alpha. There may be breaking changes before the stable release.", + "minimum": "Minimum", + "maximum": "Maximum", + "publish": "Publish", + "unpublish": "Unpublish", + "published": "Published", + "workflowLocked": "Workflow Locked", + "workflowLockedPublished": "Published workflows are locked for editing.\nYou can unpublish the workflow to edit it, or make a copy of it.", + "workflowLockedDuringPublishing": "Workflow is locked while configuring for publishing.", + "selectOutputNode": "Select Output Node", + "changeOutputNode": "Change Output Node", + "publishedWorkflowOutputs": "Outputs", + "publishedWorkflowInputs": "Inputs", + "unpublishableInputs": "These unpublishable inputs will be omitted", + "noPublishableInputs": "No publishable inputs", + "noOutputNodeSelected": "No output node selected", + "cannotPublish": "Cannot publish workflow", + "publishWarnings": "Warnings", + "errorWorkflowHasUnsavedChanges": "Workflow has unsaved changes", + "errorWorkflowHasUnpublishableNodes": "Workflow has batch, generator, or metadata extraction nodes", + "errorWorkflowHasInvalidGraph": "Workflow graph invalid (hover Invoke button for details)", + "errorWorkflowHasNoOutputNode": "No output node selected", + "warningWorkflowHasNoPublishableInputFields": "No publishable input fields selected - published workflow will run with only default values", + "warningWorkflowHasUnpublishableInputFields": "Workflow has some unpublishable inputs - these will be omitted from the published workflow", + "publishFailed": "Publish failed", + "publishFailedDesc": "There was a problem publishing the workflow. Please try again.", + "publishSuccess": "Your workflow is being published", + "publishSuccessDesc": "Check your Project Dashboard to see its progress.", + "publishInProgress": "Publishing in progress", + "publishedWorkflowIsLocked": "Published workflow is locked", + "publishingValidationRun": "Publishing Validation Run", + "publishingValidationRunInProgress": "Publishing validation run in progress.", + "publishedWorkflowsLocked": "Published workflows are locked and cannot be edited or run. Either unpublish the workflow or save a copy to edit or run this workflow.", + "selectingOutputNode": "Selecting output node", + "selectingOutputNodeDesc": "Click a node to select it as the workflow's output node." + } + }, + "controlLayers": { + "regional": "Regional", + "global": "Global", + "canvas": "Canvas", + "bookmark": "Bookmark for Quick Switch", + "fitBboxToLayers": "Fit Bbox To Layers", + "fitBboxToMasks": "Fit Bbox To Masks", + "removeBookmark": "Remove Bookmark", + "saveCanvasToGallery": "Save Canvas to Gallery", + "saveBboxToGallery": "Save Bbox to Gallery", + "saveLayerToAssets": "Save Layer to Assets", + "exportCanvasToPSD": "Export Canvas to PSD", + "cropLayerToBbox": "Crop Layer to Bbox", + "savedToGalleryOk": "Saved to Gallery", + "savedToGalleryError": "Error saving to gallery", + "regionCopiedToClipboard": "{{region}} Copied to Clipboard", + "copyRegionError": "Error copying {{region}}", + "newGlobalReferenceImageOk": "Created Global Reference Image", + "newGlobalReferenceImageError": "Problem Creating Global Reference Image", + "newRegionalReferenceImageOk": "Created Regional Reference Image", + "newRegionalReferenceImageError": "Problem Creating Regional Reference Image", + "newControlLayerOk": "Created Control Layer", + "newControlLayerError": "Problem Creating Control Layer", + "newRasterLayerOk": "Created Raster Layer", + "newRasterLayerError": "Problem Creating Raster Layer", + "pullBboxIntoLayerOk": "Bbox Pulled Into Layer", + "pullBboxIntoLayerError": "Problem Pulling BBox Into Layer", + "pullBboxIntoReferenceImageOk": "Bbox Pulled Into ReferenceImage", + "pullBboxIntoReferenceImageError": "Problem Pulling BBox Into ReferenceImage", + "addAdjustments": "Add Adjustments", + "removeAdjustments": "Remove Adjustments", + "workflowIntegration": { + "title": "Run Workflow on Canvas", + "description": "Select a workflow with a Canvas Output node and an image parameter to run on the current canvas layer. You can adjust parameters before executing. The result will be added back to the canvas.", + "execute": "Execute Workflow", + "executing": "Executing...", + "runWorkflow": "Run Workflow", + "filteringWorkflows": "Filtering workflows...", + "loadingWorkflows": "Loading workflows...", + "noWorkflowsFound": "No workflows found.", + "noWorkflowsWithImageField": "No compatible workflows found. A workflow needs a Form Builder with an image input field and a Canvas Output node.", + "selectWorkflow": "Select Workflow", + "selectPlaceholder": "Choose a workflow...", + "unnamedWorkflow": "Unnamed Workflow", + "loadingParameters": "Loading workflow parameters...", + "noFormBuilderError": "This workflow has no form builder and cannot be used. Please select a different workflow.", + "imageFieldSelected": "This field will receive the canvas image", + "imageFieldNotSelected": "Click to use this field for canvas image", + "executionStarted": "Workflow execution started", + "executionStartedDescription": "The result will appear in the staging area when complete.", + "executionFailed": "Failed to execute workflow" + }, + "compositeOperation": { + "label": "Blend Mode", + "add": "Add Blend Mode", + "remove": "Remove Blend Mode", + "blendModes": { + "source-over": "Normal", + "color": "Color", + "hue": "Hue", + "overlay": "Overlay", + "soft-light": "Soft Light", + "hard-light": "Hard Light", + "screen": "Screen", + "color-burn": "Color Burn", + "color-dodge": "Color Dodge", + "multiply": "Multiply", + "darken": "Darken", + "lighten": "Lighten", + "difference": "Difference", + "luminosity": "Luminosity", + "saturation": "Saturation" + } + }, + "booleanOps": { + "label": "Boolean Operations", + "intersect": "Intersect", + "cutout": "Cut Out", + "cutaway": "Cut Away", + "exclude": "Exclude" + }, + "adjustments": { + "simple": "Simple", + "curves": "Curves", + "heading": "Adjustments", + "expand": "Expand adjustments", + "collapse": "Collapse adjustments", + "brightness": "Brightness", + "contrast": "Contrast", + "saturation": "Saturation", + "temperature": "Temperature", + "tint": "Tint", + "sharpness": "Sharpness", + "finish": "Finish", + "reset": "Reset", + "master": "Master" + }, + "regionIsEmpty": "Selected region is empty", + "mergeVisible": "Merge Visible", + "mergeDown": "Merge Down", + "mergeVisibleOk": "Merged layers", + "mergeVisibleError": "Error merging layers", + "mergingLayers": "Merging layers", + "clearHistory": "Clear History", + "bboxOverlay": "Show Bbox Overlay", + "ruleOfThirds": "Show Rule of Thirds", + "newSession": "New Session", + "clearCaches": "Clear Caches", + "recalculateRects": "Recalculate Rects", + "clipToBbox": "Clip Strokes to Bbox", + "extractRegion": "Extract Region", + "outputOnlyMaskedRegions": "Output Only Generated Regions", + "addLayer": "Add Layer", + "duplicate": "Duplicate", + "moveToFront": "Move to Front", + "moveToBack": "Move to Back", + "moveForward": "Move Forward", + "moveBackward": "Move Backward", + "width": "Width", + "autoNegative": "Auto Negative", + "enableAutoNegative": "Enable Auto Negative", + "disableAutoNegative": "Disable Auto Negative", + "deletePrompt": "Delete Prompt", + "deleteReferenceImage": "Delete Reference Image", + "disableReferenceImage": "Disable Reference Image", + "enableReferenceImage": "Enable Reference Image", + "showHUD": "Show HUD", + "rectangle": "Rectangle", + "maskFill": "Mask Fill", + "maskLayerEmpty": "Mask layer is empty", + "extractMaskedAreaFailed": "Unable to extract masked area.", + "extractMaskedAreaMissingData": "Cannot extract: image or mask data is missing.", + "addPositivePrompt": "Add $t(controlLayers.prompt)", + "addNegativePrompt": "Add $t(controlLayers.negativePrompt)", + "addReferenceImage": "Add $t(controlLayers.referenceImage)", + "addImageNoise": "Add $t(controlLayers.imageNoise)", + "addRasterLayer": "Add $t(controlLayers.rasterLayer)", + "addControlLayer": "Add $t(controlLayers.controlLayer)", + "addInpaintMask": "Add $t(controlLayers.inpaintMask)", + "addRegionalGuidance": "Add $t(controlLayers.regionalGuidance)", + "addGlobalReferenceImage": "Add $t(controlLayers.globalReferenceImage)", + "addDenoiseLimit": "Add $t(controlLayers.denoiseLimit)", + "rasterLayer": "Raster Layer", + "controlLayer": "Control Layer", + "inpaintMask": "Inpaint Mask", + "invertMask": "Invert Mask", + "invertRegion": "Invert Region", + "regionalGuidance": "Regional Guidance", + "referenceImageRegional": "Reference Image (Regional)", + "referenceImageGlobal": "Reference Image (Global)", + "asRasterLayer": "As $t(controlLayers.rasterLayer)", + "asRasterLayerResize": "As $t(controlLayers.rasterLayer) (Resize)", + "asControlLayer": "As $t(controlLayers.controlLayer)", + "asControlLayerResize": "As $t(controlLayers.controlLayer) (Resize)", + "invalidReferenceImage": "Invalid Reference Image:", + "referenceImage": "Reference Image", + "removeImageFromCollection": "Remove Image from Collection", + "selectRefImage": "Select Ref Image", + "maxRefImages": "Max Ref Images", + "useAsReferenceImage": "Use as Reference Image", + "regionalReferenceImage": "Regional Reference Image", + "globalReferenceImage": "Global Reference Image", + "sendingToCanvas": "Staging Generations on Canvas", + "sendingToGallery": "Sending Generations to Gallery", + "sendToGallery": "Send To Gallery", + "sendToGalleryDesc": "Pressing Invoke generates and saves a unique image to your gallery.", + "sendToCanvas": "Send To Canvas", + "newLayerFromImage": "New Layer from Image", + "text": { + "font": "Font", + "size": "Size", + "bold": "Bold", + "italic": "Italic", + "underline": "Underline", + "strikethrough": "Strikethrough", + "alignLeft": "Align Left", + "alignCenter": "Align Center", + "alignRight": "Align Right", + "px": "px", + "lineHeight": "Spacing", + "lineHeightDense": "Dense", + "lineHeightNormal": "Normal", + "lineHeightSpacious": "Spacious" + }, + "newCanvasFromImage": "New Canvas from Image", + "newImg2ImgCanvasFromImage": "New Img2Img from Image", + "copyToClipboard": "Copy to Clipboard", + "sendToCanvasDesc": "Pressing Invoke stages your work in progress on the canvas.", + "viewProgressInViewer": "View progress and outputs in the Image Viewer.", + "viewProgressOnCanvas": "View progress and stage outputs on the Canvas.", + "rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)", + "rasterLayer_withCount_other": "Raster Layers", + "controlLayer_withCount_one": "$t(controlLayers.controlLayer)", + "controlLayer_withCount_other": "Control Layers", + "inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)", + "inpaintMask_withCount_other": "Inpaint Masks", + "regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)", + "regionalGuidance_withCount_other": "Regional Guidance", + "globalReferenceImage_withCount_one": "$t(controlLayers.globalReferenceImage)", + "globalReferenceImage_withCount_other": "Global Reference Images", + "opacity": "Opacity", + "regionalGuidance_withCount_hidden": "Regional Guidance ({{count}} hidden)", + "controlLayers_withCount_hidden": "Control Layers ({{count}} hidden)", + "rasterLayers_withCount_hidden": "Raster Layers ({{count}} hidden)", + "globalReferenceImages_withCount_hidden": "Global Reference Images ({{count}} hidden)", + "inpaintMasks_withCount_hidden": "Inpaint Masks ({{count}} hidden)", + "regionalGuidance_withCount_visible": "Regional Guidance ({{count}})", + "controlLayers_withCount_visible": "Control Layers ({{count}})", + "rasterLayers_withCount_visible": "Raster Layers ({{count}})", + "globalReferenceImages_withCount_visible": "Global Reference Images ({{count}})", + "inpaintMasks_withCount_visible": "Inpaint Masks ({{count}})", + "layer_one": "Layer", + "layer_other": "Layers", + "layer_withCount_one": "Layer ({{count}})", + "layer_withCount_other": "Layers ({{count}})", + "convertRasterLayerTo": "Convert $t(controlLayers.rasterLayer) To", + "convertControlLayerTo": "Convert $t(controlLayers.controlLayer) To", + "convertInpaintMaskTo": "Convert $t(controlLayers.inpaintMask) To", + "convertRegionalGuidanceTo": "Convert $t(controlLayers.regionalGuidance) To", + "copyRasterLayerTo": "Copy $t(controlLayers.rasterLayer) To", + "copyControlLayerTo": "Copy $t(controlLayers.controlLayer) To", + "copyInpaintMaskTo": "Copy $t(controlLayers.inpaintMask) To", + "copyRegionalGuidanceTo": "Copy $t(controlLayers.regionalGuidance) To", + "newRasterLayer": "New $t(controlLayers.rasterLayer)", + "newControlLayer": "New $t(controlLayers.controlLayer)", + "newInpaintMask": "New $t(controlLayers.inpaintMask)", + "newRegionalGuidance": "New $t(controlLayers.regionalGuidance)", + "pasteTo": "Paste To", + "pasteToAssets": "Assets", + "pasteToAssetsDesc": "Paste to Assets", + "pasteToBbox": "Bbox", + "pasteToBboxDesc": "New Layer (in Bbox)", + "pasteToCanvas": "Canvas", + "pasteToCanvasDesc": "New Layer (in Canvas)", + "pastedTo": "Pasted to {{destination}}", + "transparency": "Transparency", + "enableTransparencyEffect": "Enable Transparency Effect", + "disableTransparencyEffect": "Disable Transparency Effect", + "hidingType": "Hiding {{type}}", + "showingType": "Showing {{type}}", + "showNonRasterLayers": "Show Non-Raster Layers (Shift+H)", + "hideNonRasterLayers": "Hide Non-Raster Layers (Shift+H)", + "dynamicGrid": "Dynamic Grid", + "logDebugInfo": "Log Debug Info", + "locked": "Locked", + "unlocked": "Unlocked", + "transparencyLocked": "Transparency Locked", + "transparencyUnlocked": "Transparency Unlocked", + "deleteSelected": "Delete Selected", + "stagingOnCanvas": "Staging images on", + "replaceLayer": "Replace Layer", + "pullBboxIntoLayer": "Pull Bbox into Layer", + "pullBboxIntoReferenceImage": "Pull Bbox into Reference Image", + "showProgressOnCanvas": "Show Progress on Canvas", + "useImage": "Use Image", + "prompt": "Prompt", + "negativePrompt": "Negative Prompt", + "beginEndStepPercentShort": "Begin/End %", + "weight": "Weight", + "newGallerySession": "New Gallery Session", + "newGallerySessionDesc": "This will clear the canvas and all settings except for your model selection. Generations will be sent to the gallery.", + "newCanvasSession": "New Canvas Session", + "newCanvasSessionDesc": "This will clear the canvas and all settings except for your model selection. Generations will be staged on the canvas.", + "resetCanvasLayers": "Reset Canvas Layers", + "resetGenerationSettings": "Reset Generation Settings", + "replaceCurrent": "Replace Current", + "controlLayerEmptyState": "Upload an image, drag an image from the gallery onto this layer, pull the bounding box into this layer, or draw on the canvas to get started.", + "referenceImageEmptyStateWithCanvasOptions": "Upload an image, drag an image from the gallery onto this Reference Image or pull the bounding box into this Reference Image to get started.", + "referenceImageEmptyState": "Upload an image or drag an image from the gallery onto this Reference Image to get started.", + "uploadOrDragAnImage": "Drag an image from the gallery or upload an image.", + "imageNoise": "Image Noise", + "denoiseLimit": "Denoise Limit", + "warnings": { + "problemsFound": "Problems found", + "unsupportedModel": "layer not supported for selected base model", + "controlAdapterNoModelSelected": "no Control Layer model selected", + "controlAdapterIncompatibleBaseModel": "incompatible Control Layer base model", + "controlAdapterNoControl": "no control selected/drawn", + "ipAdapterNoModelSelected": "no Reference Image model selected", + "ipAdapterIncompatibleBaseModel": "incompatible Reference Image base model", + "ipAdapterNoImageSelected": "no Reference Image image selected", + "rgNoPromptsOrIPAdapters": "no text prompts or Reference Images", + "rgNegativePromptNotSupported": "Negative Prompt not supported for selected base model", + "rgReferenceImagesNotSupported": "regional Reference Images not supported for selected base model", + "rgAutoNegativeNotSupported": "Auto-Negative not supported for selected base model", + "rgNoRegion": "no region drawn", + "fluxFillIncompatibleWithControlLoRA": "Control LoRA is not compatible with FLUX Fill", + "bboxHidden": "Bounding box is hidden (shift+o to toggle)" + }, + "errors": { + "unableToFindImage": "Unable to find image", + "unableToLoadImage": "Unable to Load Image" + }, + "controlMode": { + "controlMode": "Control Mode", + "balanced": "Balanced (recommended)", + "prompt": "Prompt", + "control": "Control", + "megaControl": "Mega Control" + }, + "ipAdapterMethod": { + "ipAdapterMethod": "Mode", + "full": "Style and Composition", + "fullDesc": "Applies visual style (colors, textures) & composition (layout, structure).", + "style": "Style (Simple)", + "styleDesc": "Applies visual style (colors, textures) without considering its layout. Previously called Style Only.", + "composition": "Composition Only", + "compositionDesc": "Replicates layout & structure while ignoring the reference's style.", + "styleStrong": "Style (Strong)", + "styleStrongDesc": "Applies a strong visual style, with a slightly reduced composition influence.", + "stylePrecise": "Style (Precise)", + "stylePreciseDesc": "Applies a precise visual style, eliminating subject influence." + }, + "fluxReduxImageInfluence": { + "imageInfluence": "Image Influence", + "lowest": "Lowest", + "low": "Low", + "medium": "Medium", + "high": "High", + "highest": "Highest" + }, + "fill": { + "fillColor": "Fill Color", + "bgFillColor": "Background Color", + "fgFillColor": "Foreground Color", + "fillStyle": "Fill Style", + "solid": "Solid", + "grid": "Grid", + "crosshatch": "Crosshatch", + "vertical": "Vertical", + "horizontal": "Horizontal", + "diagonal": "Diagonal", + "switchColors": "Switch FG/BG (X)" + }, + "gradient": { + "linear": "Linear", + "radial": "Radial", + "clip": "Clip Gradient" + }, + "lasso": { + "freehand": "Freehand", + "polygon": "Polygon", + "polygonHint": "Click to add points, click the first point to close." + }, + "shape": { + "rect": "Rect", + "oval": "Oval" + }, + "modifierHints": { + "keys": { + "control": "Ctrl", + "command": "Cmd", + "option": "Option", + "alt": "Alt", + "shift": "Shift", + "space": "Space", + "wheel": "Wheel", + "arrows": "Arrows", + "enter": "Enter", + "esc": "Esc" + }, + "labels": { + "pan": "Pan", + "moveShape": "Move shape", + "pickColor": "Pick color", + "straightLine": "Straight line", + "resizeBrush": "Resize brush", + "resizeEraser": "Resize eraser", + "erase": "Erase", + "snap45Degrees": "Snap to 45deg", + "lockAspectRatio": "Lock ratio", + "unlockAspectRatio": "Unlock ratio", + "scaleFromCenter": "Scale from center", + "fineGrid": "Fine grid", + "commitText": "Commit", + "newLine": "New line", + "cancelText": "Cancel", + "dragText": "Drag text", + "snapRotation": "Snap rotation", + "nudgeSelection": "Nudge selection" + } + }, + "tool": { + "brush": "Brush", + "eraser": "Eraser", + "shapes": "Shapes", + "rectangle": "Rectangle", + "lasso": "Lasso", + "gradient": "Gradient", + "bbox": "Bbox", + "move": "Move", + "view": "View", + "colorPicker": "Color Picker", + "text": "Text" + }, + "filter": { + "filter": "Filter", + "filters": "Filters", + "filterType": "Filter Type", + "autoProcess": "Auto Process", + "reset": "Reset", + "process": "Process", + "apply": "Apply", + "cancel": "Cancel", + "advanced": "Advanced", + "processingLayerWith": "Processing layer with the {{type}} filter.", + "forMoreControl": "For more control, click Advanced below.", + "spandrel_filter": { + "label": "Image-to-Image Model", + "description": "Run an image-to-image model on the selected layer.", + "model": "Model", + "autoScale": "Auto Scale", + "autoScaleDesc": "The selected model will be run until the target scale is reached.", + "scale": "Target Scale" + }, + "canny_edge_detection": { + "label": "Canny Edge Detection", + "description": "Generates an edge map from the selected layer using the Canny edge detection algorithm.", + "low_threshold": "Low Threshold", + "high_threshold": "High Threshold" + }, + "color_map": { + "label": "Color Map", + "description": "Create a color map from the selected layer.", + "tile_size": "Tile Size" + }, + "content_shuffle": { + "label": "Content Shuffle", + "description": "Shuffles the content of the selected layer, similar to a 'liquify' effect.", + "scale_factor": "Scale Factor" + }, + "depth_anything_depth_estimation": { + "label": "Depth Anything", + "description": "Generates a depth map from the selected layer using a Depth Anything model.", + "model_size": "Model Size", + "model_size_small": "Small", + "model_size_small_v2": "Small v2", + "model_size_base": "Base", + "model_size_large": "Large" + }, + "dw_openpose_detection": { + "label": "DW Openpose Detection", + "description": "Detects human poses in the selected layer using the DW Openpose model.", + "draw_hands": "Draw Hands", + "draw_face": "Draw Face", + "draw_body": "Draw Body" + }, + "hed_edge_detection": { + "label": "HED Edge Detection", + "description": "Generates an edge map from the selected layer using the HED edge detection model.", + "scribble": "Scribble" + }, + "lineart_anime_edge_detection": { + "label": "Lineart Anime Edge Detection", + "description": "Generates an edge map from the selected layer using the Lineart Anime edge detection model." + }, + "lineart_edge_detection": { + "label": "Lineart Edge Detection", + "description": "Generates an edge map from the selected layer using the Lineart edge detection model.", + "coarse": "Coarse" + }, + "mediapipe_face_detection": { + "label": "MediaPipe Face Detection", + "description": "Detects faces in the selected layer using the MediaPipe face detection model.", + "max_faces": "Max Faces", + "min_confidence": "Min Confidence" + }, + "mlsd_detection": { + "label": "Line Segment Detection", + "description": "Generates a line segment map from the selected layer using the MLSD line segment detection model.", + "score_threshold": "Score Threshold", + "distance_threshold": "Distance Threshold" + }, + "normal_map": { + "label": "Normal Map", + "description": "Generates a normal map from the selected layer." + }, + "pidi_edge_detection": { + "label": "PiDiNet Edge Detection", + "description": "Generates an edge map from the selected layer using the PiDiNet edge detection model.", + "scribble": "Scribble", + "quantize_edges": "Quantize Edges" + }, + "img_blur": { + "label": "Blur Image", + "description": "Blurs the selected layer.", + "blur_type": "Blur Type", + "blur_radius": "Radius", + "gaussian_type": "Gaussian", + "box_type": "Box" + }, + "img_noise": { + "label": "Noise Image", + "description": "Adds noise to the selected layer.", + "noise_type": "Noise Type", + "noise_amount": "Amount", + "gaussian_type": "Gaussian", + "salt_and_pepper_type": "Salt and Pepper", + "noise_color": "Colored Noise", + "size": "Noise Size" + }, + "adjust_image": { + "label": "Adjust Image", + "description": "Adjusts the selected channel of an image.", + "channel": "Channel", + "value_setting": "Value", + "scale_values": "Scale Values", + "red": "Red (RGBA)", + "green": "Green (RGBA)", + "blue": "Blue (RGBA)", + "alpha": "Alpha (RGBA)", + "cyan": "Cyan (CMYK)", + "magenta": "Magenta (CMYK)", + "yellow": "Yellow (CMYK)", + "black": "Black (CMYK)", + "hue": "Hue (HSV)", + "saturation": "Saturation (HSV)", + "value": "Value (HSV)", + "luminosity": "Luminosity (LAB)", + "a": "A (LAB)", + "b": "B (LAB)", + "y": "Y (YCbCr)", + "cb": "Cb (YCbCr)", + "cr": "Cr (YCbCr)" + }, + "pbr_maps": { + "label": "Create PBR Maps" + } + }, + "transform": { + "transform": "Transform", + "fitToBbox": "Fit to Bbox", + "fitMode": "Fit Mode", + "fitModeContain": "Contain", + "fitModeCover": "Cover", + "fitModeFill": "Fill", + "smoothing": "Smoothing", + "smoothingDesc": "Apply a high-quality backend resample when committing transforms.", + "smoothingMode": "Resample Mode", + "smoothingModeBilinear": "Bilinear", + "smoothingModeBicubic": "Bicubic", + "smoothingModeHamming": "Hamming", + "smoothingModeLanczos": "Lanczos", + "reset": "Reset", + "apply": "Apply", + "cancel": "Cancel" + }, + "selectObject": { + "selectObject": "Select Object", + "pointType": "Point Type", + "invertSelection": "Invert Selection", + "include": "Include", + "exclude": "Exclude", + "neutral": "Neutral", + "apply": "Apply", + "reset": "Reset", + "saveAs": "Save As", + "cancel": "Cancel", + "process": "Process", + "desc": "Select a single target object. After selection is complete, click Apply to discard everything outside the selected area, or save the selection as a new layer.", + "visualModeDesc": "Visual mode uses box and point inputs to select an object.", + "visualMode1": "Click and drag to draw a box around the object you want to select. You may get better results by drawing the box a bit larger or smaller than the object.", + "visualMode2": "Click to add a green include point, or shift-click to add a red exclude point to tell the model what to include or exclude.", + "visualMode3": "Points can be used to refine a box selection or used independently.", + "promptModeDesc": "Prompt mode uses text input to select an object.", + "promptMode1": "Type a brief description of the object you want to select.", + "promptMode2": "Use simple language, avoiding complex descriptions or multiple objects.", + "clickToAdd": "Click on the layer to add a point", + "dragToMove": "Drag a point to move it", + "clickToRemove": "Click on a point to remove it", + "model": "Model", + "segmentAnything1": "Segment Anything 1", + "segmentAnything2": "Segment Anything 2", + "prompt": "Selection Prompt" + }, + "settings": { + "snapToGrid": { + "label": "Snap to Grid", + "on": "On", + "off": "Off" + }, + "preserveMask": { + "label": "Preserve Masked Region", + "alert": "Preserving Masked Region" + }, + "saveAllImagesToGallery": { + "label": "Send New Generations to Gallery", + "alert": "Sending new generations to Gallery, bypassing Canvas" + }, + "isolatedStagingPreview": "Isolated Staging Preview", + "isolatedPreview": "Isolated Preview", + "isolatedLayerPreview": "Isolated Layer Preview", + "isolatedLayerPreviewDesc": "Whether to show only this layer when performing operations like filtering or transforming.", + "invertBrushSizeScrollDirection": "Invert Scroll for Brush Size", + "pressureSensitivity": "Pressure Sensitivity" + }, + "HUD": { + "bbox": "Bbox", + "scaledBbox": "Scaled Bbox", + "textSessionActive": "Text input is active", + "entityStatus": { + "isFiltering": "{{title}} is filtering", + "isTransforming": "{{title}} is transforming", + "isLocked": "{{title}} is locked", + "isHidden": "{{title}} is hidden", + "isDisabled": "{{title}} is disabled", + "isEmpty": "{{title}} is empty" + } + }, + "canvasContextMenu": { + "canvasGroup": "Canvas", + "saveToGalleryGroup": "Save To Gallery", + "saveCanvasToGallery": "Save Canvas To Gallery", + "saveBboxToGallery": "Save Bbox To Gallery", + "bboxGroup": "Create From Bbox", + "newGlobalReferenceImage": "New Global Reference Image", + "newRegionalReferenceImage": "New Regional Reference Image", + "newControlLayer": "New Control Layer", + "newResizedControlLayer": "New Resized Control Layer", + "newRasterLayer": "New Raster Layer", + "newInpaintMask": "New Inpaint Mask", + "newRegionalGuidance": "New Regional Guidance", + "cropCanvasToBbox": "Crop Canvas to Bbox", + "copyToClipboard": "Copy to Clipboard", + "copyCanvasToClipboard": "Copy Canvas to Clipboard", + "copyBboxToClipboard": "Copy Bbox to Clipboard" + }, + "canvasProject": { + "project": "Project", + "saveProject": "Save Canvas Project", + "loadProject": "Load Canvas Project", + "saveSuccess": "Project Saved", + "saveSuccessDesc": "Saved project with {{count}} images", + "saveError": "Failed to Save Project", + "loadSuccess": "Project Loaded", + "loadSuccessDesc": "Canvas state restored from project file", + "loadError": "Failed to Load Project", + "loadWarning": "Loading a project will replace your current canvas, including all layers, masks, reference images, and generation parameters. This action cannot be undone.", + "projectName": "Project Name" + }, + "stagingArea": { + "accept": "Accept", + "discardAll": "Discard All", + "discard": "Discard", + "previous": "Previous", + "next": "Next", + "saveToGallery": "Save To Gallery", + "hideThumbnails": "Hide Thumbnails", + "showThumbnails": "Show Thumbnails", + "showResultsOn": "Showing Results", + "showResultsOff": "Hiding Results" + }, + "autoSwitch": { + "off": "Off", + "doNotAutoSwitch": "Do not auto-switch", + "switchOnStart": "On Start", + "switchOnStartDesc": "Switch on start", + "switchOnFinish": "On Finish", + "switchOnFinishDesc": "Switch on finish" + }, + "snapshot": { + "snapshots": "Save or Load Canvas Snapshot", + "saveSnapshot": "Save Snapshot", + "restoreSnapshot": "Restore Snapshot", + "snapshotNamePlaceholder": "Snapshot name", + "save": "Save", + "delete": "Delete", + "snapshotSaved": "Snapshot \"{{name}}\" saved", + "snapshotRestored": "Snapshot \"{{name}}\" restored", + "snapshotDeleted": "Snapshot \"{{name}}\" deleted", + "snapshotSaveFailed": "Failed to save snapshot", + "snapshotRestoreFailed": "Failed to restore snapshot", + "snapshotDeleteFailed": "Failed to delete snapshot", + "snapshotMissingImages_one": "{{count}} image referenced by this snapshot no longer exists and will appear as a placeholder", + "snapshotMissingImages_other": "{{count}} images referenced by this snapshot no longer exist and will appear as placeholders", + "snapshotIncompatible": "This snapshot was created with a different version and is no longer compatible", + "overwriteSnapshotTitle": "Overwrite snapshot?", + "overwriteSnapshotMessage": "A snapshot named \"{{name}}\" already exists. Overwrite it?", + "overwrite": "Overwrite" + } + }, + "upscaling": { + "upscale": "Upscale", + "creativity": "Creativity", + "exceedsMaxSize": "Upscale settings exceed max size limit", + "exceedsMaxSizeDetails": "Max upscale limit is {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixels. Please try a smaller image or decrease your scale selection.", + "structure": "Structure", + "upscaleModel": "Upscale Model", + "postProcessingModel": "Post-Processing Model", + "scale": "Scale", + "tileControl": "Tile Control", + "tileSize": "Tile Size", + "tileOverlap": "Tile Overlap", + "postProcessingMissingModelWarning": "Visit the Model Manager to install a post-processing (image to image) model.", + "missingModelsWarning": "Visit the Model Manager to install the required models:", + "missingModelsWarningNonAdmin": "Ask your InvokeAI administrator () to install the required models:", + "mainModelDesc": "Main model (SD1.5 or SDXL architecture)", + "tileControlNetModelDesc": "Tile ControlNet model for the chosen main model architecture", + "upscaleModelDesc": "Upscale (image to image) model", + "missingUpscaleInitialImage": "Missing initial image for upscaling", + "missingUpscaleModel": "Missing upscale model", + "missingTileControlNetModel": "No valid tile ControlNet models installed", + "incompatibleBaseModel": "Unsupported main model architecture for upscaling", + "incompatibleBaseModelDesc": "Upscaling is supported for SD1.5 and SDXL architecture models only. Change the main model to enable upscaling." + }, + "stylePresets": { + "active": "Active", + "choosePromptTemplate": "Choose Prompt Template", + "clearTemplateSelection": "Clear Template Selection", + "copyTemplate": "Copy Template", + "createPromptTemplate": "Create Prompt Template", + "defaultTemplates": "Default Templates", + "deleteImage": "Delete Image", + "deleteTemplate": "Delete Template", + "deleteTemplate2": "Are you sure you want to delete this template? This cannot be undone.", + "exportPromptTemplates": "Export My Prompt Templates (CSV)", + "editTemplate": "Edit Template", + "exportDownloaded": "Export Downloaded", + "exportFailed": "Unable to generate and download CSV", + "flatten": "Flatten selected template into current prompt", + "importTemplates": "Import Prompt Templates (CSV/JSON)", + "acceptedColumnsKeys": "Accepted columns/keys:", + "nameColumn": "'name'", + "positivePromptColumn": "'prompt' or 'positive_prompt'", + "negativePromptColumn": "'negative_prompt'", + "insertPlaceholder": "Insert placeholder", + "myTemplates": "My Templates", + "name": "Name", + "negativePrompt": "Negative Prompt", + "noTemplates": "No templates", + "noMatchingTemplates": "No matching templates", + "promptTemplatesDesc1": "Prompt templates add text to the prompts you write in the prompt box.", + "promptTemplatesDesc2": "Use the placeholder string
{{placeholder}}
to specify where your prompt should be included in the template.", + "promptTemplatesDesc3": "If you omit the placeholder, the template will be appended to the end of your prompt.", + "positivePrompt": "Positive Prompt", + "preview": "Preview", + "private": "Private", + "promptTemplateCleared": "Prompt Template Cleared", + "searchByName": "Search by name", + "shared": "Shared", + "sharedTemplates": "Shared Templates", + "templateDeleted": "Prompt template deleted", + "toggleViewMode": "Toggle View Mode", + "type": "Type", + "unableToDeleteTemplate": "Unable to delete prompt template", + "updatePromptTemplate": "Update Prompt Template", + "uploadImage": "Upload Image", + "useForTemplate": "Use For Prompt Template", + "viewList": "View Template List", + "viewModeTooltip": "This is how your prompt will look with your currently selected template. To edit your prompt, click anywhere in the text box.", + "togglePromptPreviews": "Toggle Prompt Previews", + "selectPreset": "Select Style Preset", + "noMatchingPresets": "No matching presets" + }, + "ui": { + "tabs": { + "generate": "Generate", + "canvas": "Canvas", + "workflows": "Workflows", + "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)", + "models": "Models", + "modelsTab": "$t(ui.tabs.models) $t(common.tab)", + "queue": "Queue", + "upscaling": "Upscaling", + "upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)", + "customNodes": "Nodes", + "customNodesTab": "$t(ui.tabs.customNodes) $t(common.tab)", + "gallery": "Gallery" + }, + "panels": { + "launchpad": "Launchpad", + "workflowEditor": "Workflow Editor", + "imageViewer": "Viewer", + "canvas": "Canvas" + }, + "launchpad": { + "workflowsTitle": "Go deep with Workflows.", + "upscalingTitle": "Upscale and add detail.", + "canvasTitle": "Edit and refine on Canvas.", + "generateTitle": "Generate images from text prompts.", + "modelGuideText": "Want to learn what prompts work best for each model?", + "modelGuideLink": "Check out our Model Guide.", + "createNewWorkflowFromScratch": "Create a new Workflow from scratch", + "browseAndLoadWorkflows": "Browse and load existing workflows", + "addStyleRef": { + "title": "Add a Style Reference", + "description": "Add an image to transfer its look." + }, + "editImage": { + "title": "Edit Image", + "description": "Add an image to refine." + }, + "generateFromText": { + "title": "Generate from Text", + "description": "Enter a prompt and Invoke." + }, + "useALayoutImage": { + "title": "Use a Layout Image", + "description": "Add an image to control composition." + }, + "generate": { + "canvasCalloutTitle": "Looking to get more control, edit, and iterate on your images?", + "canvasCalloutLink": "Navigate to Canvas for more capabilities." + }, + "workflows": { + "description": "Workflows are reusable templates that automate image generation tasks, allowing you to quickly perform complex operations and get consistent results.", + "descriptionMultiuser": "Workflows are reusable templates that automate image generation tasks, allowing you to quickly perform complex operations and get consistent results. You may share your workflows with other users of the system by selecting 'Shared workflow' when you create or edit it.", + "learnMoreLink": "Learn more about creating workflows", + "browseTemplates": { + "title": "Browse Workflow Templates", + "description": "Choose from pre-built workflows for common tasks" + }, + "createNew": { + "title": "Create a new Workflow", + "description": "Start a new workflow from scratch" + }, + "loadFromFile": { + "title": "Load workflow from file", + "description": "Upload a workflow to start with an existing setup" + } + }, + "upscaling": { + "uploadImage": { + "title": "Upload Image to Upscale", + "description": "Click or drag an image to upscale (JPG, PNG, WebP up to 100MB)" + }, + "replaceImage": { + "title": "Replace Current Image", + "description": "Click or drag a new image to replace the current one" + }, + "imageReady": { + "title": "Image Ready", + "description": "Press Invoke to begin upscaling" + }, + "readyToUpscale": { + "title": "Ready to upscale!", + "description": "Configure your settings below, then click the Invoke button to begin upscaling your image." + }, + "upscaleModel": "Upscale Model", + "model": "Model", + "scale": "Scale", + "creativityAndStructure": { + "title": "Creativity & Structure Defaults", + "conservative": "Conservative", + "balanced": "Balanced", + "creative": "Creative", + "artistic": "Artistic" + }, + "helpText": { + "promptAdvice": "When upscaling, use a prompt that describes the medium and style. Avoid describing specific content details in the image.", + "styleAdvice": "Upscaling works best with the general style of your image." + } + } + } + }, + "system": { + "enableLogging": "Enable Logging", + "logLevel": { + "logLevel": "Log Level", + "trace": "Trace", + "debug": "Debug", + "info": "Info", + "warn": "Warn", + "error": "Error", + "fatal": "Fatal" + }, + "logNamespaces": { + "logNamespaces": "Log Namespaces", + "dnd": "Drag and Drop", + "gallery": "Gallery", + "models": "Models", + "config": "Config", + "canvas": "Canvas", + "generation": "Generation", + "workflows": "Workflows", + "system": "System", + "events": "Events", + "queue": "Queue", + "metadata": "Metadata" + } + }, + "newUserExperience": { + "toGetStartedLocal": "To get started, make sure to download or import models needed to run Invoke. Then, enter a prompt in the box and click Invoke to generate your first image. Select a prompt template to improve results. You can choose to save your images directly to the Gallery or edit them to the Canvas.", + "toGetStarted": "To get started, enter a prompt in the box and click Invoke to generate your first image. Select a prompt template to improve results. You can choose to save your images directly to the Gallery or edit them to the Canvas.", + "toGetStartedWorkflow": "To get started, fill in the fields on the left and press Invoke to generate your image. Want to explore more workflows? Click the folder icon next to the workflow title to see a list of other templates you can try.", + "toGetStartedNonAdmin": "To get started, ask your InvokeAI administrator () to install the AI models needed to run Invoke. Then, enter a prompt in the box and click Invoke to generate your first image. Select a prompt template to improve results. You can choose to save your images directly to the Gallery or edit them to the Canvas.", + "gettingStartedSeries": "Want more guidance? Check out our Getting Started Series for tips on unlocking the full potential of the Invoke Studio.", + "lowVRAMMode": "For best performance, follow our Low VRAM guide.", + "noModelsInstalled": "It looks like you don't have any models installed! You can download a starter model bundle or import models.", + "noModelsInstalledAskAdmin": "Ask your administrator to install some." + }, + "whatsNew": { + "whatsNewInInvoke": "What's New in Invoke", + "items": [ + "New model types: Qwen Image, Qwen Image Edit, Anima.", + "Support for hosted models: Gemini (Nano Banana), GPT Image, Qwen, Seedream, Wan", + "Private and shared image boards and workflows in multiuser mode", + "Canvas lasso tool, save/restore function, and the ability to hide those pesky preview tiles", + "Custom node manager", + "Redesigned download queue" + ], + "readReleaseNotes": "Read Release Notes", + "readTheDocs": "Read the Docs", + "readDocumentation": "Read Invoke Documentation", + "watchUiUpdatesOverview": "Watch UI Updates Overview" + }, + "cropper": { + "cropImage": "Crop Image", + "aspectRatio": "Aspect Ratio", + "free": "Free", + "mouseWheelZoom": "Mouse wheel: Zoom", + "spaceDragPan": "Space + Drag: Pan", + "dragCropBoxToAdjust": "Drag crop box or handles to adjust" + }, + "supportVideos": { + "supportVideos": "Support Videos", + "gettingStarted": "Getting Started", + "gettingStartedPlaylist": "Getting Started playlist", + "studioSessionsPlaylist": "Studio Sessions playlist", + "discord": "Discord", + "github": "GitHub", + "watch": "Watch", + "studioSessionsDesc": "Join our to participate in the live sessions and ask questions. Sessions are uploaded to the playlist the following week.", + "videos": { + "gettingStarted": { + "title": "Getting Started with Invoke", + "description": "Complete video series covering everything you need to know to get started with Invoke, from creating your first image to advanced techniques." + }, + "studioSessions": { + "title": "Studio Sessions", + "description": "Deep dive sessions exploring advanced Invoke features, creative workflows, and community discussions." + } + } + }, + "customNodes": { + "title": "Custom Nodes", + "installTitle": "Install Node Pack", + "gitUrl": "Git Repository URL", + "gitUrlLabel": "Repository URL", + "gitUrlPlaceholder": "https://github.com/user/node-pack.git", + "install": "Install", + "installing": "Installing", + "installSuccess": "Node pack installed", + "installFailed": "Installation failed", + "installError": "An unexpected error occurred during installation.", + "securityWarning": "Custom nodes execute code on your system. Only install node packs from authors you trust. Malicious nodes could harm your system or compromise your data.", + "installDescription": "Clones the repository into your nodes directory. Workflow files (.json) are imported into your library. Python dependencies (requirements.txt or pyproject.toml) are NOT installed automatically — follow the node pack's documentation to install them manually.", + "dependenciesRequiredTitle": "Manual dependency install required", + "dependenciesRequiredDescription": "'{{name}}' includes a {{file}}. Follow the node pack's documentation to install its Python dependencies before using its nodes.", + "uninstall": "Uninstall", + "reload": "Reload", + "reloading": "Reloading", + "noNodePacks": "No custom node packs installed.", + "scanFolder": "Scan Folder", + "scanFolderDescription": "Node packs placed in the nodes directory are automatically detected at startup. Use the Reload button to detect newly added packs without restarting.", + "nodesDirectory": "Nodes directory", + "installQueue": "Install Log", + "queueEmpty": "No recent install activity.", + "name": "Name", + "message": "Message", + "nodeCount_one": "{{count}} node", + "nodeCount_other": "{{count}} nodes", + "uninstalled": "Uninstalled" + } +} diff --git a/invokeai/frontend/web/public/locales/es.json b/invokeai/frontend/web/public/locales/es.json new file mode 100644 index 00000000000..8f68ea585c2 --- /dev/null +++ b/invokeai/frontend/web/public/locales/es.json @@ -0,0 +1,980 @@ +{ + "common": { + "hotkeysLabel": "Atajos de teclado", + "languagePickerLabel": "Selector de idioma", + "reportBugLabel": "Reportar errores", + "settingsLabel": "Ajustes", + "img2img": "Imagen a Imagen", + "nodes": "Flujos de trabajo", + "upload": "Subir imagen", + "load": "Cargar", + "statusDisconnected": "Desconectado", + "githubLabel": "Github", + "discordLabel": "Discord", + "back": "Atrás", + "loading": "Cargando", + "postprocessing": "Postprocesamiento", + "txt2img": "De texto a imagen", + "accept": "Aceptar", + "cancel": "Cancelar", + "linear": "Lineal", + "random": "Aleatorio", + "openInNewTab": "Abrir en una nueva pestaña", + "dontAskMeAgain": "No me preguntes de nuevo", + "areYouSure": "¿Estas seguro?", + "batch": "Administrador de lotes", + "modelManager": "Administrador de modelos", + "communityLabel": "Comunidad", + "direction": "Dirección", + "ai": "Ia", + "add": "Añadir", + "auto": "Automático", + "copyError": "Error $t(gallery.copy)", + "details": "Detalles", + "or": "o", + "checkpoint": "Punto de control", + "controlNet": "ControlNet", + "aboutHeading": "Sea dueño de su poder creativo", + "advanced": "Avanzado", + "data": "Fecha", + "delete": "Borrar", + "copy": "Copiar", + "beta": "Beta", + "on": "En", + "aboutDesc": "¿Utilizas Invoke para trabajar? Mira aquí:", + "installed": "Instalado", + "green": "Verde", + "editor": "Editor", + "orderBy": "Ordenar por", + "file": "Archivo", + "saveAs": "Guardar Como", + "somethingWentWrong": "Algo salió mal", + "selected": "Seleccionado", + "tab": "Tabulador", + "positivePrompt": "Prompt Positivo", + "negativePrompt": "Prompt Negativo", + "error": "Error", + "format": "formato", + "unknown": "Desconocido", + "input": "Entrada", + "template": "Plantilla", + "red": "Rojo", + "alpha": "Transparencia", + "outputs": "Resultados", + "learnMore": "Aprende más", + "enabled": "Activado", + "disabled": "Desactivado", + "folder": "Carpeta", + "updated": "Actualizado", + "created": "Creado", + "save": "Guardar", + "unknownError": "Error Desconocido", + "blue": "Azul", + "clipboard": "Portapapeles", + "loadingImage": "Cargando la imagen", + "inpaint": "inpaint", + "ipAdapter": "Adaptador IP", + "t2iAdapter": "Adaptador T2I", + "apply": "Aplicar", + "openInViewer": "Abrir en el visor", + "off": "Apagar", + "generating": "Generando", + "ok": "De acuerdo", + "placeholderSelectAModel": "Seleccionar un modelo", + "reset": "Restablecer", + "none": "Ninguno", + "new": "Nuevo", + "dontShowMeThese": "No mostrar estos", + "loadingModel": "Cargando el modelo", + "view": "Ver", + "edit": "Editar", + "safetensors": "Safetensors", + "toResolve": "Para resolver", + "outpaint": "outpaint", + "simple": "Sencillo", + "close": "Cerrar", + "board": "Tablero", + "crop": "Cortar" + }, + "gallery": { + "galleryImageSize": "Tamaño de la imagen", + "gallerySettings": "Ajustes de la galería", + "autoSwitchNewImages": "Auto seleccionar Imágenes nuevas", + "deleteImage_one": "Eliminar Imagen", + "deleteImage_many": "Eliminar {{count}} Imágenes", + "deleteImage_other": "Eliminar {{count}} Imágenes", + "deleteImagePermanent": "Las imágenes eliminadas no se pueden restaurar.", + "autoAssignBoardOnClick": "Asignar automática tableros al hacer clic", + "gallery": "Galería", + "noImageSelected": "Sin imágenes seleccionadas", + "bulkDownloadRequestFailed": "Error al preparar la descarga", + "oldestFirst": "La más antigua primero", + "sideBySide": "conjuntamente", + "selectForCompare": "Seleccionar para comparar", + "alwaysShowImageSizeBadge": "Mostrar siempre las dimensiones de la imagen", + "currentlyInUse": "Esta imagen se utiliza actualmente con las siguientes funciones:", + "selectAllOnPage": "Seleccionar todo en la página", + "bulkDownloadFailed": "Error en la descarga", + "compareHelp2": "Presione M para recorrer los modos de comparación.", + "move": "Mover", + "copy": "Copiar", + "drop": "Gota", + "displayBoardSearch": "Tablero de búsqueda", + "deleteSelection": "Borrar selección", + "downloadSelection": "Descargar selección", + "openInViewer": "Abrir en el visor", + "searchImages": "Búsqueda por metadatos", + "swapImages": "Intercambiar imágenes", + "sortDirection": "Orden de clasificación", + "showStarredImagesFirst": "Mostrar imágenes destacadas primero", + "go": "Ir", + "bulkDownloadRequested": "Preparando la descarga", + "image": "imagen", + "compareHelp4": "Presione Z o Esc para salir.", + "viewerImage": "Ver imagen", + "dropOrUpload": "$t(gallery.drop) o cargar", + "displaySearch": "Buscar imagen", + "download": "Descargar", + "exitBoardSearch": "Finalizar búsqueda", + "exitSearch": "Salir de la búsqueda de imágenes", + "featuresWillReset": "Si elimina esta imagen, dichas funciones se restablecerán inmediatamente.", + "loading": "Cargando", + "newestFirst": "La más nueva primero", + "unstarImage": "Dejar de ser favorita", + "bulkDownloadRequestedDesc": "Su solicitud de descarga se está preparando. Esto puede tardar unos minutos.", + "hover": "Desplazar", + "compareHelp1": "Mantenga presionada la tecla Alt mientras hace clic en una imagen de la galería o utiliza las teclas de flecha para cambiar la imagen de comparación.", + "stretchToFit": "Estirar para encajar", + "exitCompare": "Salir de la comparación", + "starImage": "Imágenes favoritas", + "dropToUpload": "$t(gallery.drop) para cargar", + "slider": "Deslizador", + "assetsTab": "Archivos que has cargado para utilizarlos en tus proyectos.", + "imagesTab": "Imágenes que ha creado y guardado en Invoke.", + "compareImage": "Comparar imagen", + "boardsSettings": "Ajustes de los tableros", + "imagesSettings": "Configuración de imágenes de la galería", + "compareHelp3": "Presione C para intercambiar las imágenes comparadas.", + "showArchivedBoards": "Mostrar paneles archivados" + }, + "modelManager": { + "modelManager": "Gestor de Modelos", + "model": "Modelo", + "modelUpdated": "Modelo actualizado", + "manual": "Manual", + "name": "Nombre", + "description": "Descripción", + "config": "Configurar", + "width": "Ancho", + "height": "Alto", + "addModel": "Añadir Modelo", + "availableModels": "Modelos disponibles", + "search": "Búsqueda", + "load": "Cargar", + "active": "activo", + "selected": "Seleccionado", + "delete": "Eliminar", + "deleteModel": "Eliminar Modelo", + "deleteConfig": "Eliminar Configuración", + "deleteMsg1": "¿Estás seguro de que deseas eliminar este modelo de InvokeAI?", + "deleteMsg2": "Esto eliminará el modelo del disco si está en la carpeta raíz de InvokeAI. Si está utilizando una ubicación personalizada, el modelo NO se eliminará del disco.", + "convertToDiffusersHelpText4": "Este proceso se realiza una sola vez. Puede tardar entre 30 y 60 segundos dependiendo de las especificaciones de tu ordenador.", + "convert": "Convertir", + "convertToDiffusers": "Convertir en difusores", + "convertToDiffusersHelpText1": "Este modelo se convertirá al formato 🧨 Difusores.", + "convertToDiffusersHelpText2": "Este proceso sustituirá su entrada del Gestor de Modelos por la versión de Difusores del mismo modelo.", + "convertToDiffusersHelpText3": "Tu archivo del punto de control en el disco se eliminará si está en la carpeta raíz de InvokeAI. Si está en una ubicación personalizada, NO se eliminará.", + "convertToDiffusersHelpText5": "Por favor, asegúrate de tener suficiente espacio en el disco. Los modelos generalmente varían entre 2 GB y 7 GB de tamaño.", + "convertToDiffusersHelpText6": "¿Desea transformar este modelo?", + "modelConverted": "Modelo adaptado", + "alpha": "Alfa", + "allModels": "Todos los modelos", + "repo_id": "Identificador del repositorio", + "none": "ninguno", + "vae": "VAE", + "variant": "Variante", + "baseModel": "Modelo básico", + "modelConversionFailed": "Conversión al modelo fallida", + "selectModel": "Seleccionar un modelo", + "modelUpdateFailed": "Error al actualizar el modelo", + "convertingModelBegin": "Convirtiendo el modelo. Por favor, espere.", + "modelDeleted": "Modelo eliminado", + "modelDeleteFailed": "Error al borrar el modelo", + "settings": "Ajustes", + "syncModels": "Sincronizar las plantillas", + "clipEmbed": "Incrustar CLIP", + "addModels": "Añadir modelos", + "advanced": "Avanzado", + "clipGEmbed": "Incrustar CLIP-G", + "cancel": "Cancelar", + "clipLEmbed": "Incrustar CLIP-L" + }, + "parameters": { + "images": "Imágenes", + "steps": "Pasos", + "cfgScale": "Escala CFG", + "width": "Ancho", + "height": "Alto", + "seed": "Semilla", + "shuffle": "Semilla aleatoria", + "noiseThreshold": "Umbral de Ruido", + "perlinNoise": "Ruido Perlin", + "type": "Tipo", + "strength": "Fuerza", + "upscaling": "Aumento de resolución", + "scale": "Escala", + "imageFit": "Ajuste tamaño de imagen inicial al tamaño objetivo", + "scaleBeforeProcessing": "Redimensionar antes de procesar", + "scaledWidth": "Ancho escalado", + "scaledHeight": "Alto escalado", + "infillMethod": "Método de relleno", + "tileSize": "Tamaño del mosaico", + "usePrompt": "Usar Entrada", + "useSeed": "Usar Semilla", + "useAll": "Usar Todo", + "info": "Información", + "symmetry": "Simetría", + "copyImage": "Copiar la imagen", + "general": "General", + "denoisingStrength": "Intensidad de la eliminación del ruido", + "seamlessXAxis": "Eje X sin juntas", + "seamlessYAxis": "Eje Y sin juntas", + "scheduler": "Programador", + "positivePromptPlaceholder": "Prompt Positivo", + "negativePromptPlaceholder": "Prompt Negativo", + "controlNetControlMode": "Modo de control", + "clipSkip": "Omitir el CLIP", + "maskBlur": "Desenfoque de máscara", + "patchmatchDownScaleSize": "Reducir a escala", + "coherenceMode": "Modo" + }, + "settings": { + "models": "Modelos", + "displayInProgress": "Mostrar las imágenes del progreso", + "confirmOnDelete": "Confirmar antes de eliminar", + "resetWebUI": "Restablecer interfaz web", + "resetWebUIDesc1": "Al restablecer la interfaz web, solo se restablece la caché local del navegador de sus imágenes y la configuración guardada. No se elimina ninguna imagen de su disco duro.", + "resetWebUIDesc2": "Si las imágenes no se muestran en la galería o algo más no funciona, intente restablecer antes de reportar un incidente en GitHub.", + "resetComplete": "Se ha restablecido la interfaz web.", + "general": "General", + "developer": "Desarrollador", + "antialiasProgressImages": "Imágenes del progreso de Antialias", + "showProgressInViewer": "Mostrar las imágenes del progreso en el visor", + "ui": "Interfaz del usuario", + "generation": "Generación", + "beta": "Beta", + "reloadingIn": "Recargando en", + "intermediatesClearedFailed": "Error limpiando los intermediarios", + "intermediatesCleared_one": "Borrado {{count}} intermediario", + "intermediatesCleared_many": "Borrados {{count}} intermediarios", + "intermediatesCleared_other": "Borrados {{count}} intermediarios" + }, + "toast": { + "uploadFailed": "Error al subir archivo", + "imageCopied": "Imágen copiada", + "parametersNotSet": "Parámetros no recuperados", + "serverError": "Error en el servidor", + "canceled": "Procesando la cancelación", + "connected": "Conectado al servidor", + "uploadFailedInvalidUploadDesc": "Deben ser imágenes PNG o JPEG.", + "parameterSet": "Parámetro recuperado", + "parameterNotSet": "Parámetro no recuperado", + "problemCopyingImage": "No se puede copiar la imagen", + "errorCopied": "Error al copiar", + "baseModelChanged": "Modelo base cambiado", + "addedToBoard": "Se agregó a los activos del panel {{name}}", + "baseModelChangedCleared_one": "Borrado o desactivado {{count}} submodelo incompatible", + "baseModelChangedCleared_many": "Borrados o desactivados {{count}} submodelos incompatibles", + "baseModelChangedCleared_other": "Borrados o desactivados {{count}} submodelos incompatibles", + "addedToUncategorized": "Añadido a los activos del tablero $t(boards.uncategorized)", + "imagesWillBeAddedTo": "Las imágenes subidas se añadirán a los activos del panel {{boardName}}.", + "layerCopiedToClipboard": "Capa copiada en el portapapeles" + }, + "accessibility": { + "invokeProgressBar": "Activar la barra de progreso", + "reset": "Reiniciar", + "uploadImage": "Cargar imagen", + "previousImage": "Imagen anterior", + "nextImage": "Siguiente imagen", + "menu": "Menú", + "about": "Acerca de", + "createIssue": "Crear un problema", + "resetUI": "Interfaz de usuario $t(accessibility.reset)", + "mode": "Modo", + "submitSupportTicket": "Enviar Ticket de Soporte", + "toggleRightPanel": "Activar o desactivar el panel derecho (G)", + "toggleLeftPanel": "Activar o desactivar el panel izquierdo (T)", + "uploadImages": "Cargar imagen(es)" + }, + "nodes": { + "zoomInNodes": "Acercar", + "hideMinimapnodes": "Ocultar el minimapa", + "fitViewportNodes": "Ajustar la vista", + "zoomOutNodes": "Alejar", + "showMinimapnodes": "Mostrar el minimapa", + "reloadNodeTemplates": "Recargar las plantillas de nodos", + "loadWorkflow": "Cargar el flujo de trabajo", + "downloadWorkflow": "Descargar el flujo de trabajo en un archivo JSON", + "boardAccessError": "No se puede encontrar el panel {{board_id}}, se está restableciendo al valor predeterminado" + }, + "boards": { + "autoAddBoard": "Agregar panel automáticamente", + "changeBoard": "Cambiar el panel", + "clearSearch": "Borrar la búsqueda", + "deleteBoard": "Borrar el panel", + "selectBoard": "Seleccionar un panel", + "uncategorized": "Sin categoría", + "cancel": "Cancelar", + "addBoard": "Agregar un panel", + "movingImagesToBoard_one": "Moviendo {{count}} imagen al panel:", + "movingImagesToBoard_many": "Moviendo {{count}} imágenes al panel:", + "movingImagesToBoard_other": "Moviendo {{count}} imágenes al panel:", + "bottomMessage": "Al eliminarlas imágenes, se restablecerán las funcionalidades que actualmente las estén utilizando.", + "deleteBoardAndImages": "Borrar el panel y las imágenes", + "loading": "Cargando...", + "deletedBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar. Al Seleccionar 'Borrar solo el panel' transferirá las imágenes a un estado sin categorizar.", + "move": "Mover", + "menuItemAutoAdd": "Agregar automáticamente a este panel", + "searchBoard": "Buscando paneles…", + "topMessage": "Este panel contiene imágenes utilizadas en las siguientes funciones:", + "downloadBoard": "Descargar panel", + "deleteBoardOnly": "Borrar solo el panel", + "myBoard": "Mi panel", + "noMatching": "Sin paneles coincidentes", + "imagesWithCount_one": "{{count}} imagen", + "imagesWithCount_many": "{{count}} imágenes", + "imagesWithCount_other": "{{count}} imágenes", + "assetsWithCount_one": "{{count}} activo", + "assetsWithCount_many": "{{count}} activos", + "assetsWithCount_other": "{{count}} activos", + "addPrivateBoard": "Agregar un panel privado", + "addSharedBoard": "Añadir panel compartido", + "boards": "Paneles", + "archiveBoard": "Archivar panel", + "archived": "Archivado", + "selectedForAutoAdd": "Seleccionado para agregar automáticamente", + "unarchiveBoard": "Desarchivar el panel", + "noBoards": "No hay paneles {{boardType}}", + "shared": "Paneles compartidos", + "deletedPrivateBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar. Al elegir \"Eliminar solo el panel\", las imágenes se colocarán en un estado privado y sin categoría para el creador de la imagen.", + "private": "Paneles privados", + "updateBoardError": "No se pudo actualizar el panel", + "pause": "Pausa", + "resume": "Reanudar", + "restartFailed": "Reinicio fallido", + "restartFile": "Reiniciar archivo", + "restartRequired": "Reinicio requerido", + "resumeRefused": "Reanudación rechazada por el servidor. Reinicio requerido.", + "uncategorizedImages": "Imágenes sin categoría", + "deleteAllUncategorizedImages": "Eliminar todas las imágenes sin categoría", + "deletedImagesCannotBeRestored": "Las imágenes eliminadas no pueden ser restauradas.", + "hideBoards": "Ocultar tableros", + "locateInGalery": "Ubicar en galeria", + "viewBoards": "Ver paneles" + }, + "accordions": { + "compositing": { + "title": "Composición", + "infillTab": "Relleno", + "coherenceTab": "Parámetros de la coherencia" + }, + "generation": { + "title": "Generación" + }, + "image": { + "title": "Imagen" + }, + "control": { + "title": "Control" + }, + "advanced": { + "options": "$t(accordions.advanced.title) opciones", + "title": "Avanzado" + } + }, + "ui": { + "tabs": { + "canvas": "Lienzo", + "queue": "Cola", + "workflows": "Flujos de trabajo", + "models": "Modelos", + "modelsTab": "$t(ui.tabs.models) $t(common.tab)", + "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)", + "upscaling": "Upscaling", + "gallery": "Galería", + "upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)" + } + }, + "queue": { + "back": "Atrás", + "front": "Delante", + "batchQueuedDesc_one": "Se agregó {{count}} sesión a {{direction}} la cola", + "batchQueuedDesc_many": "Se agregaron {{count}} sesiones a {{direction}} la cola", + "batchQueuedDesc_other": "Se agregaron {{count}} sesiones a {{direction}} la cola", + "clearQueueAlertDialog": "Al vaciar la cola se cancela inmediatamente cualquier elemento de procesamiento y se vaciará la cola por completo. Los filtros pendientes se cancelarán.", + "time": "Tiempo", + "clearFailed": "Error al vaciar la cola", + "cancelFailed": "Error al cancelar el elemento", + "resumeFailed": "Error al reanudar el proceso", + "pause": "Pausar", + "pauseTooltip": "Pausar el proceso", + "cancelBatchSucceeded": "Lote cancelado", + "pruneSucceeded": "Se purgaron {{item_count}} elementos completados de la cola", + "pruneFailed": "Error al purgar la cola", + "cancelBatchFailed": "Error al cancelar los lotes", + "pauseFailed": "Error al pausar el proceso", + "status": "Estado", + "origin": "Origen", + "destination": "Destino", + "generations_one": "Generación", + "generations_many": "Generaciones", + "generations_other": "Generaciones", + "resume": "Reanudar", + "queueEmpty": "Cola vacía", + "cancelItem": "Cancelar elemento", + "cancelBatch": "Cancelar lote", + "openQueue": "Abrir la cola", + "completed": "Completado", + "enqueueing": "Añadir lotes a la cola", + "clear": "Limpiar", + "pauseSucceeded": "Proceso pausado", + "resumeSucceeded": "Proceso reanudado", + "resumeTooltip": "Reanudar proceso", + "cancel": "Cancelar", + "cancelTooltip": "Cancelar artículo actual", + "pruneTooltip": "Purgar {{item_count}} elementos completados", + "batchQueued": "Lote en cola", + "pending": "Pendiente", + "item": "Elemento", + "total": "Total", + "in_progress": "En proceso", + "failed": "Fallido", + "completedIn": "Completado en", + "upscaling": "Upscaling", + "canvas": "Lienzo", + "generation": "Generación", + "workflows": "Flujo de trabajo", + "other": "Otro", + "queueFront": "Añadir al principio de la cola", + "gallery": "Galería", + "session": "Sesión", + "notReady": "La cola aún no está lista", + "graphQueued": "Gráfico en cola", + "clearQueueAlertDialog2": "¿Estás seguro que deseas vaciar la cola?", + "next": "Siguiente", + "iterations_one": "Interacción", + "iterations_many": "Interacciones", + "iterations_other": "Interacciones", + "current": "Actual", + "queue": "Cola", + "queueBack": "Añadir a la cola", + "cancelSucceeded": "Elemento cancelado", + "clearTooltip": "Cancelar y limpiar todos los elementos", + "clearSucceeded": "Cola vaciada", + "canceled": "Cancelado", + "batch": "Lote", + "graphFailedToQueue": "Error al poner el gráfico en cola", + "batchFailedToQueue": "Error al poner en cola el lote", + "prompts_one": "Prompt", + "prompts_many": "Prompts", + "prompts_other": "Prompts", + "prune": "Eliminar" + }, + "controlLayers": { + "layer_one": "Capa", + "layer_many": "Capas", + "layer_other": "Capas", + "copyToClipboard": "Copiar al portapapeles" + }, + "whatsNew": { + "readReleaseNotes": "Leer las notas de la versión", + "watchRecentReleaseVideos": "Ver videos de versiones recientes", + "whatsNewInInvoke": "Novedades en Invoke", + "items": [ + "SD 3.5: compatibilidad con SD 3.5 Medium y Large." + ] + }, + "invocationCache": { + "enableFailed": "Error al activar la cache", + "cacheSize": "Tamaño de la caché", + "hits": "Accesos a la caché", + "invocationCache": "Caché", + "misses": "Errores de la caché", + "clear": "Limpiar", + "maxCacheSize": "Tamaño máximo de la caché", + "enableSucceeded": "Cache activada", + "clearFailed": "Error al borrar la cache", + "enable": "Activar", + "useCache": "Uso de la caché", + "disableSucceeded": "Caché desactivada", + "clearSucceeded": "Caché borrada", + "disable": "Desactivar", + "disableFailed": "Error al desactivar la caché" + }, + "hrf": { + "hrf": "Solución de alta resolución", + "metadata": { + "enabled": "Corrección de alta resolución activada", + "strength": "Forzar la corrección de alta resolución", + "method": "Método de corrección de alta resolución" + } + }, + "prompt": { + "addPromptTrigger": "Añadir activador de los avisos", + "compatibleEmbeddings": "Incrustaciones compatibles", + "noMatchingTriggers": "No hay activadores coincidentes" + }, + "hotkeys": { + "hotkeys": "Atajo del teclado", + "canvas": { + "selectViewTool": { + "desc": "Selecciona la herramienta de Visualización.", + "title": "Visualización" + }, + "cancelFilter": { + "title": "Cancelar el filtro", + "desc": "Cancelar el filtro pendiente." + }, + "applyTransform": { + "title": "Aplicar la transformación", + "desc": "Aplicar la transformación pendiente a la capa seleccionada." + }, + "applyFilter": { + "desc": "Aplicar el filtro pendiente a la capa seleccionada.", + "title": "Aplicar filtro" + }, + "selectBrushTool": { + "title": "Pincel", + "desc": "Selecciona la herramienta pincel." + }, + "selectBboxTool": { + "desc": "Seleccionar la herramienta de selección del marco.", + "title": "Selección del marco" + }, + "selectMoveTool": { + "desc": "Selecciona la herramienta Mover.", + "title": "Mover" + }, + "selectRectTool": { + "title": "Rectángulo", + "desc": "Selecciona la herramienta Rectángulo." + }, + "decrementToolWidth": { + "title": "Reducir el ancho de la herramienta", + "desc": "Disminuye la anchura de la herramienta pincel o goma de borrar, según la que esté seleccionada." + }, + "incrementToolWidth": { + "title": "Incrementar la anchura de la herramienta", + "desc": "Aumenta la anchura de la herramienta pincel o goma de borrar, según la que esté seleccionada." + }, + "fitBboxToCanvas": { + "title": "Ajustar bordes al lienzo", + "desc": "Escala y posiciona la vista para ajustarla a los bodes." + }, + "fitLayersToCanvas": { + "title": "Ajustar capas al lienzo", + "desc": "Escala y posiciona la vista para que se ajuste a todas las capas visibles." + }, + "resetSelected": { + "title": "Restablecer capa", + "desc": "Restablecer la capa seleccionada. Solo se aplica a Máscara de retoque y Guía regional." + }, + "setZoomTo400Percent": { + "desc": "Ajuste la aplicación del lienzo al 400%.", + "title": "Ampliar al 400%" + }, + "transformSelected": { + "desc": "Transformar la capa seleccionada.", + "title": "Transformar" + }, + "selectColorPickerTool": { + "title": "Selector de color", + "desc": "Seleccione la herramienta de selección de color." + }, + "selectEraserTool": { + "title": "Borrador", + "desc": "Selecciona la herramienta Borrador." + }, + "setZoomTo100Percent": { + "title": "Ampliar al 100%", + "desc": "Ajuste ampliar el lienzo al 100%." + }, + "undo": { + "title": "Deshacer", + "desc": "Deshacer la última acción en el lienzo." + }, + "nextEntity": { + "desc": "Seleccione la siguiente capa de la lista.", + "title": "Capa siguiente" + }, + "redo": { + "title": "Rehacer", + "desc": "Rehacer la última acción en el lienzo." + }, + "prevEntity": { + "title": "Capa anterior", + "desc": "Seleccione la capa anterior de la lista." + }, + "title": "Lienzo", + "setZoomTo200Percent": { + "title": "Ampliar al 200%", + "desc": "Ajuste la ampliación del lienzo al 200%." + }, + "setZoomTo800Percent": { + "title": "Ampliar al 800%", + "desc": "Ajuste la ampliación del lienzo al 800%." + }, + "filterSelected": { + "desc": "Filtra la capa seleccionada. Solo se aplica a las capas Ráster y Control.", + "title": "Filtrar" + }, + "cancelTransform": { + "title": "Cancelar transformación", + "desc": "Cancelar la transformación pendiente." + }, + "deleteSelected": { + "title": "Borrar la capa", + "desc": "Borrar la capa seleccionada." + }, + "quickSwitch": { + "desc": "Cambiar entre las dos últimas capas seleccionadas. Si una capa está seleccionada, cambia siempre entre ella y la última capa no seleccionada.", + "title": "Cambio rápido de capa" + } + }, + "app": { + "selectModelsTab": { + "title": "Seleccione la pestaña Modelos", + "desc": "Selecciona la pestaña Modelos." + }, + "focusPrompt": { + "desc": "Mueve el foco del cursor a la indicación positiva.", + "title": "Enfoque" + }, + "toggleLeftPanel": { + "title": "Alternar panel izquierdo", + "desc": "Mostrar u ocultar el panel izquierdo." + }, + "selectQueueTab": { + "title": "Seleccione la pestaña Cola", + "desc": "Seleccione la pestaña Cola." + }, + "selectCanvasTab": { + "title": "Seleccione la pestaña Lienzo", + "desc": "Selecciona la pestaña Lienzo." + }, + "clearQueue": { + "title": "Vaciar cola", + "desc": "Cancelar y variar todos los elementos de la cola." + }, + "selectUpscalingTab": { + "title": "Selecciona la pestaña Ampliar", + "desc": "Selecciona la pestaña Aumento de escala." + }, + "togglePanels": { + "desc": "Muestra u oculta los paneles izquierdo y derecho a la vez.", + "title": "Alternar paneles" + }, + "toggleRightPanel": { + "title": "Alternar panel derecho", + "desc": "Mostrar u ocultar el panel derecho." + }, + "invokeFront": { + "desc": "Pone en cola la solicitud de compilación y la agrega al principio de la cola.", + "title": "Invocar (frente)" + }, + "cancelQueueItem": { + "title": "Cancelar", + "desc": "Cancelar el elemento de la cola que se está procesando." + }, + "invoke": { + "desc": "Pone en cola la solicitud de compilación y la agrega al final de la cola.", + "title": "Invocar" + }, + "title": "Aplicación", + "selectWorkflowsTab": { + "title": "Seleccione la pestaña Flujos de trabajo", + "desc": "Selecciona la pestaña Flujos de trabajo." + }, + "resetPanelLayout": { + "title": "Reiniciar la posición del panel", + "desc": "Restablece los paneles izquierdo y derecho a su tamaño y disposición por defecto." + } + }, + "workflows": { + "addNode": { + "title": "Añadir nodo", + "desc": "Abrir añadir nodo." + }, + "selectAll": { + "title": "Seleccionar todo", + "desc": "Seleccione todos los nodos y enlaces." + }, + "deleteSelection": { + "desc": "Borrar todos los nodos y enlaces seleccionados.", + "title": "Borrar" + }, + "undo": { + "desc": "Deshaga la última acción.", + "title": "Deshacer" + }, + "redo": { + "desc": "Rehacer la última acción.", + "title": "Rehacer" + }, + "pasteSelection": { + "desc": "Pegar nodos y bordes copiados.", + "title": "Pegar" + }, + "title": "Flujos de trabajo", + "copySelection": { + "desc": "Copiar nodos y bordes seleccionados.", + "title": "Copiar" + }, + "pasteSelectionWithEdges": { + "desc": "Pega los nodos copiados, los enlaces y todos los enlaces conectados a los nodos copiados.", + "title": "Pegar con enlaces" + } + }, + "viewer": { + "useSize": { + "title": "Usar dimensiones", + "desc": "Utiliza las dimensiones de la imagen actual como el tamaño del borde." + }, + "remix": { + "title": "Remezcla", + "desc": "Recupera todos los metadatos excepto la semilla de la imagen actual." + }, + "loadWorkflow": { + "desc": "Carga el flujo de trabajo guardado de la imagen actual (si tiene uno).", + "title": "Cargar flujo de trabajo" + }, + "recallAll": { + "desc": "Recupera todos los metadatos de la imagen actual.", + "title": "Recuperar todos los metadatos" + }, + "recallPrompts": { + "desc": "Recuerde las indicaciones positivas y negativas de la imagen actual.", + "title": "Recordatorios" + }, + "recallSeed": { + "title": "Recuperar semilla", + "desc": "Recupera la semilla de la imagen actual." + }, + "runPostprocessing": { + "title": "Ejecutar posprocesamiento", + "desc": "Ejecutar el posprocesamiento seleccionado en la imagen actual." + }, + "toggleMetadata": { + "title": "Mostrar/ocultar los metadatos", + "desc": "Mostrar u ocultar la superposición de metadatos de la imagen actual." + }, + "nextComparisonMode": { + "desc": "Desplácese por los modos de comparación.", + "title": "Siguiente comparación" + }, + "title": "Visor de imágenes", + "toggleViewer": { + "title": "Mostrar/Ocultar el visor de imágenes", + "desc": "Mostrar u ocultar el visor de imágenes. Solo disponible en la pestaña Lienzo." + }, + "swapImages": { + "title": "Intercambiar imágenes en la comparación", + "desc": "Intercambia las imágenes que se están comparando." + } + }, + "gallery": { + "clearSelection": { + "title": "Limpiar selección", + "desc": "Borrar la selección actual, si hay alguna." + }, + "galleryNavUp": { + "title": "Subir", + "desc": "Navega hacia arriba en la cuadrícula de la galería y selecciona esa imagen. Si estás en la parte superior de la página, ve a la página anterior." + }, + "galleryNavLeft": { + "title": "Izquierda", + "desc": "Navegue hacia la izquierda en la rejilla de la galería, seleccionando esa imagen. Si está en la primera imagen de la fila, vaya a la fila anterior. Si está en la primera imagen de la página, vaya a la página anterior." + }, + "galleryNavDown": { + "title": "Bajar", + "desc": "Navegue hacia abajo en la parrilla de la galería, seleccionando esa imagen. Si se encuentra al final de la página, vaya a la página siguiente." + }, + "galleryNavRight": { + "title": "A la derecha", + "desc": "Navegue hacia la derecha en la rejilla de la galería, seleccionando esa imagen. Si está en la última imagen de la fila, vaya a la fila siguiente. Si está en la última imagen de la página, vaya a la página siguiente." + }, + "galleryNavUpAlt": { + "desc": "Igual que arriba, pero selecciona la imagen de comparación, abriendo el modo de comparación si no está ya abierto.", + "title": "Arriba (Comparar imagen)" + }, + "deleteSelection": { + "desc": "Borrar todas las imágenes seleccionadas. Por defecto, se le pedirá que confirme la eliminación. Si las imágenes están actualmente en uso en la aplicación, se te avisará.", + "title": "Borrar" + }, + "title": "Galería", + "selectAllOnPage": { + "title": "Seleccionar todo en la página", + "desc": "Seleccionar todas las imágenes en la página actual." + } + }, + "searchHotkeys": "Buscar teclas de acceso rápido", + "noHotkeysFound": "Sin teclas de acceso rápido", + "clearSearch": "Limpiar la búsqueda" + }, + "metadata": { + "guidance": "Orientación", + "createdBy": "Creado por", + "noImageDetails": "Sin detalles en la imagen", + "cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)", + "height": "Altura", + "imageDimensions": "Dimensiones de la imagen", + "seamlessXAxis": "Eje X sin juntas", + "seamlessYAxis": "Eje Y sin juntas", + "generationMode": "Modo de generación", + "scheduler": "Programador", + "width": "Ancho", + "Threshold": "Umbral de ruido", + "canvasV2Metadata": "Lienzo", + "metadata": "Metadatos", + "model": "Modelo", + "allPrompts": "Todas las indicaciones", + "cfgScale": "Escala CFG", + "imageDetails": "Detalles de la imagen", + "negativePrompt": "Indicación negativa", + "noMetaData": "Sin metadatos", + "parameterSet": "Parámetro {{parameter}} establecido", + "vae": "Autocodificador", + "workflow": "Flujo de trabajo", + "seed": "Semilla", + "strength": "Forzar imagen a imagen", + "recallParameters": "Parámetros de recuperación", + "steps": "Pasos", + "noRecallParameters": "Sin parámetros para recuperar" + }, + "system": { + "logLevel": { + "debug": "Depurar", + "info": "Información", + "warn": "Advertir", + "fatal": "Grave", + "error": "Error", + "trace": "Rastro", + "logLevel": "Nivel del registro" + }, + "enableLogging": "Activar registro", + "logNamespaces": { + "workflows": "Flujos de trabajo", + "system": "Sistema", + "metadata": "Metadatos", + "gallery": "Galería", + "logNamespaces": "Espacios para los nombres de registro", + "generation": "Generación", + "events": "Eventos", + "canvas": "Lienzo", + "config": "Ajustes", + "models": "Modelos", + "queue": "Cola" + } + }, + "newUserExperience": { + "toGetStarted": "Para empezar, introduzca un mensaje en el cuadro y haga clic en Invocar para generar su primera imagen. Seleccione una plantilla para mejorar los resultados. Puede elegir guardar sus imágenes directamente en Galería o editarlas en Lienzo.", + "noModelsInstalled": "Parece que no tienes ningún modelo instalado", + "gettingStartedSeries": "¿Desea más orientación? Consulte nuestra Serie de introducción para obtener consejos sobre cómo aprovechar todo el potencial de Invoke Studio.", + "toGetStartedLocal": "Para empezar, asegúrate de descargar o importar los modelos necesarios para ejecutar Invoke. A continuación, introduzca un mensaje en el cuadro y haga clic en Invocar para generar su primera imagen. Seleccione una plantilla para mejorar los resultados. Puede elegir guardar sus imágenes directamente en Galería o editarlas en el Lienzo." + }, + "auth": { + "login": { + "title": "Iniciar sesión en InvokeAI", + "email": "Email", + "emailPlaceholder": "Email", + "password": "Contraseña", + "passwordPlaceholder": "Contraseña", + "rememberMe": "Recordarme por 7 días", + "signIn": "Iniciar sesión", + "signingIn": "Iniciando sesión...", + "loginFailed": "Inicio de sesión fallido. Por favor revise sus credenciales." + }, + "setup": { + "title": "Bienvenido a InvokeAI", + "subtitle": "Configure su cuenta de administrador para empezar", + "email": "Email", + "emailPlaceholder": "admin@example.com", + "emailHelper": "Este será su nombre de usuario para iniciar sesión", + "displayName": "Nombre para mostrar", + "displayNamePlaceholder": "Administrador", + "displayNameHelper": "Su nombre como se mostrará en la aplicación", + "password": "Contraseña", + "passwordPlaceholder": "Contraseña", + "passwordHelper": "Debe tener al menos 8 caracteres con mayúsculas, minúsculas y números", + "passwordTooShort": "La contraseña debe tener al menos 8 caracteres", + "passwordMissingRequirements": "La contraseña debe contener mayúsculas, minúsculas y numeros", + "confirmPassword": "Confirmar contraseña", + "confirmPasswordPlaceholder": "Confirmar contraseña", + "passwordsDoNotMatch": "Las contraseñas no coinciden", + "createAccount": "Crear cuenta de administrador", + "creatingAccount": "Configurando...", + "setupFailed": "Configuración fallida. Por favor intente nuevamente.", + "passwordHelperRelaxed": "Ingrese una contraseña (se mostrará la fortaleza)" + }, + "userMenu": "Menu de usuario", + "admin": "Administrador", + "logout": "Cerrar Sesión", + "adminOnlyFeature": "Esta funcionalidad solo esta disponible para administradores.", + "profile": { + "menuItem": "Mi perfil", + "title": "Mi perfil", + "email": "Email", + "emailReadOnly": "La dirección de email no puede ser cambiada", + "displayName": "Nombre para mostrar", + "displayNamePlaceholder": "Su nombre", + "changePassword": "Cambiar contraseña", + "currentPassword": "Contraseña Actual", + "currentPasswordPlaceholder": "Contraseña Actual", + "newPassword": "Nueva contraseña", + "newPasswordPlaceholder": "Nueva contraseña", + "confirmPassword": "Confirmar nueva contraseña", + "confirmPasswordPlaceholder": "Confirmar nueva contraseña", + "passwordsDoNotMatch": "Las contraseñas no coinciden", + "saveSuccess": "Perfil actualizado correctamente", + "saveFailed": "Falló el guardado del perfil. Por favor intente nuevamente." + }, + "userManagement": { + "menuItem": "Administración de usuario", + "title": "Administración de usuario", + "email": "Email", + "emailPlaceholder": "user@example.com", + "displayName": "Nombre para mostrar", + "displayNamePlaceholder": "Nombre para mostrar", + "password": "Contraseña", + "passwordPlaceholder": "Contraseña", + "newPassword": "Nueva contraseña", + "newPasswordPlaceholder": "Deje en blanco para conservar la contraseña actual", + "role": "Rol", + "status": "Estado", + "actions": "Acciones", + "isAdmin": "Administrador", + "user": "Usuario", + "you": "Tu", + "createUser": "Crear usuario", + "editUser": "Editar usuario", + "deleteUser": "Eliminar usuario", + "deleteConfirm": "Esta seguro que desea eliminar {{name}}? Esta accion no se podrá revertir.", + "generatePassword": "Generar contraseña robusta", + "showPassword": "Mostrar contraseña", + "hidePassword": "Ocultar contraseña", + "activate": "Activar", + "deactivate": "Desactivar", + "saveFailed": "Fallo al guardar usuario. Por favor intente nuevamente.", + "deleteFailed": "Fallo al borrar usuario. Por favor intente nuevamente.", + "loadFailed": "Fallo al cargar usuarios.", + "back": "Atras", + "cannotDeleteSelf": "Usted no puede eliminar su propia cuenta", + "cannotDeactivateSelf": "Usted no puede desactivar su propia cuenta" + }, + "passwordStrength": { + "weak": "Contraseña debil", + "moderate": "Contraseña moderada", + "strong": "Contraseña fuerte" + } + } +} diff --git a/invokeai/frontend/web/public/locales/fi.json b/invokeai/frontend/web/public/locales/fi.json new file mode 100644 index 00000000000..54e5a666605 --- /dev/null +++ b/invokeai/frontend/web/public/locales/fi.json @@ -0,0 +1,57 @@ +{ + "accessibility": { + "reset": "Resetoi", + "uploadImage": "Lataa kuva", + "invokeProgressBar": "Invoken edistymispalkki", + "nextImage": "Seuraava kuva", + "previousImage": "Edellinen kuva", + "uploadImages": "Lähetä Kuva(t)" + }, + "common": { + "languagePickerLabel": "Kielen valinta", + "hotkeysLabel": "Pikanäppäimet", + "reportBugLabel": "Raportoi Bugista", + "settingsLabel": "Asetukset", + "githubLabel": "Github", + "discordLabel": "Discord", + "upload": "Lataa", + "img2img": "Kuva kuvaksi", + "nodes": "Solmut", + "postprocessing": "Jälkikäsitellään", + "cancel": "Peruuta", + "accept": "Hyväksy", + "load": "Lataa", + "back": "Takaisin", + "statusDisconnected": "Yhteys katkaistu", + "loading": "Ladataan", + "txt2img": "Teksti kuvaksi" + }, + "gallery": { + "galleryImageSize": "Kuvan koko", + "gallerySettings": "Gallerian asetukset", + "autoSwitchNewImages": "Vaihda uusiin kuviin automaattisesti" + }, + "modelManager": { + "t5Encoder": "T5-kooderi", + "qwen3Encoder": "Qwen3-kooderi", + "zImageVae": "VAE (valinnainen)", + "zImageQwen3Encoder": "Qwen3-kooderi (valinnainen)", + "zImageQwen3SourcePlaceholder": "Pakollinen, jos VAE/Enkooderi on tyhjä", + "flux2KleinVae": "VAE (valinnainen)", + "flux2KleinQwen3Encoder": "Qwen3-kooderi (valinnainen)" + }, + "auth": { + "login": { + "title": "Kirjaudu sisään InvokeAI:hin", + "password": "Salasana", + "passwordPlaceholder": "Salasana", + "signIn": "Kirjaudu sisään", + "signingIn": "Kirjaudutaan sisään...", + "loginFailed": "Kirjautuminen epäonnistui. Tarkista käyttäjätunnuksesi tiedot." + }, + "setup": { + "title": "Tervetuloa InvokeAI:hin", + "subtitle": "Määritä ensimmäiseksi järjestelmänvalvojan tili" + } + } +} diff --git a/invokeai/frontend/web/public/locales/fr.json b/invokeai/frontend/web/public/locales/fr.json new file mode 100644 index 00000000000..4d5d9cfcb77 --- /dev/null +++ b/invokeai/frontend/web/public/locales/fr.json @@ -0,0 +1,2262 @@ +{ + "common": { + "hotkeysLabel": "Raccourcis clavier", + "languagePickerLabel": "Langue", + "reportBugLabel": "Signaler un bug", + "settingsLabel": "Paramètres", + "img2img": "Image vers Image", + "nodes": "Workflows", + "upload": "Importer", + "load": "Charger", + "back": "Retour", + "statusDisconnected": "Hors ligne", + "discordLabel": "Discord", + "githubLabel": "Github", + "accept": "Accepter", + "cancel": "Annuler", + "loading": "Chargement", + "txt2img": "Texte vers Image", + "postprocessing": "Post-Traitement", + "file": "Fichier", + "orderBy": "Trier par", + "add": "Ajouter", + "dontAskMeAgain": "Ne plus me demander", + "outputs": "Sorties", + "unknown": "Inconnu", + "editor": "Éditeur", + "error": "Erreur", + "installed": "Installé", + "format": "format", + "input": "Entrée", + "linear": "Linéaire", + "learnMore": "En savoir plus", + "modelManager": "Gestionnaire de modèle", + "openInNewTab": "Ouvrir dans un nouvel onglet", + "somethingWentWrong": "Une erreur s'est produite", + "created": "Créé", + "tab": "Onglet", + "folder": "Dossier", + "selected": "Sélectionné", + "save": "Enregistrer", + "updated": "Mis à jour", + "random": "Aléatoire", + "unknownError": "Erreur inconnue", + "red": "Rouge", + "green": "Vert", + "delete": "Supprimer", + "simple": "Simple", + "template": "Template", + "advanced": "Avancé", + "copy": "Copier", + "saveAs": "Enregistrer sous", + "blue": "Bleu", + "alpha": "Alpha", + "enabled": "Activé", + "disabled": "Désactivé", + "direction": "Direction", + "aboutHeading": "Possédez Votre Pouvoir Créatif", + "ai": "ia", + "safetensors": "Safetensors", + "apply": "Appliquer", + "communityLabel": "Communauté", + "loadingImage": "Chargement de l'Image", + "view": "Visualisateur", + "beta": "Beta", + "on": "Activé", + "batch": "Gestionaire de Lots", + "outpaint": "Extension", + "openInViewer": "Ouvrir dans le Visualisateur", + "edit": "Édition", + "off": "Désactivé", + "areYouSure": "Êtes-vous sûr ?", + "data": "Donnée", + "details": "Détails", + "placeholderSelectAModel": "Séléctionner un modèle", + "reset": "Réinitialiser", + "none": "Aucun", + "new": "Nouveau", + "dontShowMeThese": "Ne pas me montrer ceci", + "auto": "Auto", + "or": "ou", + "checkpoint": "Point de sauvegarde", + "ipAdapter": "IP Adapter", + "t2iAdapter": "T2I Adapter", + "inpaint": "Retouche", + "toResolve": "À résoudre", + "aboutDesc": "Utilisez vous Invoke pour le travail ? Consultez :", + "copyError": "$t(gallery.copy) Erreur", + "controlNet": "ControlNet", + "positivePrompt": "Prompt Positif", + "negativePrompt": "Prompt Négatif", + "ok": "Ok", + "close": "Fermer", + "clipboard": "Presse-papier", + "loadingModel": "Chargement du modèle", + "generating": "En Génération", + "warnings": "Alertes", + "layout": "Disposition", + "row": "Ligne", + "column": "Colonne", + "start": "Commencer", + "board": "Planche", + "count": "Quantité", + "step": "Étape", + "end": "Fin", + "min": "Min", + "max": "Max", + "values": "Valeurs", + "seed": "Graine", + "combinatorial": "Combinatoire" + }, + "gallery": { + "galleryImageSize": "Taille de l'image", + "gallerySettings": "Paramètres de la galerie", + "autoSwitchNewImages": "Basculer automatiquement vers de nouvelles images", + "bulkDownloadRequestedDesc": "Votre demande de téléchargement est en cours de traitement. Cela peut prendre quelques instants.", + "deleteSelection": "Supprimer la sélection", + "selectAllOnPage": "Séléctionner toute la page", + "featuresWillReset": "Si vous supprimez cette image, ces fonctionnalités vont être réinitialisés.", + "loading": "Chargement", + "sortDirection": "Direction de tri", + "sideBySide": "Côte-à-Côte", + "hover": "Au passage de la souris", + "alwaysShowImageSizeBadge": "Toujours montrer le badge de taille de l'Image", + "gallery": "Galerie", + "bulkDownloadRequestFailed": "Problème lors de la préparation du téléchargement", + "copy": "Copier", + "autoAssignBoardOnClick": "Assigner automatiquement une Planche lors du clic", + "dropToUpload": "$t(gallery.drop) pour Importer", + "dropOrUpload": "$t(gallery.drop) ou Importer", + "oldestFirst": "Plus Ancien en premier", + "deleteImagePermanent": "Les Images supprimées ne peuvent pas être restorées.", + "displaySearch": "Recherche d'Image", + "exitBoardSearch": "Sortir de la recherche de Planche", + "go": "Aller", + "newestFirst": "Plus Récents en permier", + "showStarredImagesFirst": "Monter les Images partagées en premier", + "bulkDownloadFailed": "Téléchargement échoué", + "bulkDownloadRequested": "Préparation du téléchargement", + "compareImage": "Comparer l'Image", + "openInViewer": "Ouvrir dans le Visualiseur", + "showArchivedBoards": "Montrer les Planches archivées", + "selectForCompare": "Séléctionner pour comparaison", + "exitCompare": "Sortir de la comparaison", + "compareHelp2": "Appuyez sur M pour faire défiler les modes de comparaison.", + "swapImages": "Échanger les Images", + "move": "Déplacer", + "compareHelp1": "Maintenir Alt lors du clic d'une image dans la galerie ou en utilisant les flèches du clavier pour changer l'Image à comparer.", + "compareHelp3": "Appuyer sur C pour échanger les images à comparer.", + "image": "image", + "currentlyInUse": "Cette image est actuellement utilisée dans ces fonctionalités :", + "starImage": "Marquer l'Image", + "download": "Téléchargement", + "deleteImage_one": "Supprimer l'Image", + "deleteImage_many": "Supprimer {{count}} Images", + "deleteImage_other": "Supprimer {{count}} Images", + "displayBoardSearch": "Recherche dans la Planche", + "searchImages": "Chercher par Métadonnées", + "slider": "Curseur", + "stretchToFit": "Étirer pour remplir", + "compareHelp4": "Appuyer sur Z ou Esc pour sortir.", + "drop": "Déposer", + "noImageSelected": "Pas d'Image séléctionnée", + "downloadSelection": "Télécharger la sélection", + "exitSearch": "Sortir de la recherche d'Image", + "unstarImage": "Retirer le marquage de l'Image", + "viewerImage": "Visualisation de l'Image", + "imagesSettings": "Paramètres des images de la galerie", + "assetsTab": "Fichiers que vous avez importés pour vos projets.", + "imagesTab": "Images que vous avez créées et enregistrées dans Invoke.", + "boardsSettings": "Paramètres des planches", + "assets": "Ressources", + "images": "Images" + }, + "modelManager": { + "modelManager": "Gestionnaire de modèle", + "model": "Modèle", + "allModels": "Tous les modèles", + "modelUpdated": "Modèle mis à jour", + "manual": "Manuel", + "name": "Nom", + "description": "Description", + "config": "Config", + "repo_id": "ID de dépôt", + "width": "Largeur", + "height": "Hauteur", + "addModel": "Ajouter un modèle", + "availableModels": "Modèles disponibles", + "search": "Rechercher", + "load": "Charger", + "active": "actif", + "selected": "Sélectionné", + "delete": "Supprimer", + "deleteModel": "Supprimer le modèle", + "deleteConfig": "Supprimer la configuration", + "deleteMsg1": "Voulez-vous vraiment supprimer ce modèle de InvokeAI ?", + "deleteMsg2": "Cela SUPPRIMERA le modèle du disque s'il se trouve dans le dossier racine d'InvokeAI. Si vous utilisez un emplacement personnalisé, le modèle NE SERA PAS supprimé du disque.", + "convert": "Convertir", + "convertToDiffusersHelpText2": "Ce processus remplacera votre entrée dans le gestionaire de modèles par la version Diffusers du même modèle.", + "convertToDiffusersHelpText1": "Ce modèle sera converti au format 🧨 Diffusers.", + "huggingFaceHelper": "Si plusieurs modèles sont trouvés dans ce dépôt, vous serez invité à en sélectionner un à installer.", + "convertToDiffusers": "Convertir en Diffusers", + "convertToDiffusersHelpText5": "Veuillez vous assurer que vous disposez de suffisamment d'espace disque. La taille des modèles varient généralement entre 2 Go et 7 Go.", + "convertToDiffusersHelpText4": "C'est un processus executé une unique fois. Cela peut prendre environ 30 à 60 secondes en fonction des spécifications de votre ordinateur.", + "alpha": "Alpha", + "modelConverted": "Modèle Converti", + "convertToDiffusersHelpText3": "Votre fichier de point de contrôle sur le disque SERA supprimé s'il se trouve dans le dossier racine d'InvokeAI. S'il est dans un emplacement personnalisé, alors il NE SERA PAS supprimé.", + "convertToDiffusersHelpText6": "Souhaitez-vous convertir ce modèle ?", + "modelConversionFailed": "Échec de la conversion du modèle", + "none": "aucun", + "selectModel": "Sélectionner le modèle", + "modelDeleted": "Modèle supprimé", + "vae": "VAE", + "baseModel": "Modèle de Base", + "convertingModelBegin": "Conversion du modèle. Veuillez patienter.", + "modelDeleteFailed": "Échec de la suppression du modèle", + "modelUpdateFailed": "Échec de la mise à jour du modèle", + "variant": "Variante", + "syncModels": "Synchroniser les Modèles", + "settings": "Paramètres", + "predictionType": "Type de Prédiction", + "advanced": "Avancé", + "modelType": "Type de modèle", + "vaePrecision": "Précision VAE", + "noModelSelected": "Aucun modèle sélectionné", + "typePhraseHere": "Écrire une phrase ici", + "cancel": "Annuler", + "defaultSettingsSaved": "Paramètres par défaut enregistrés", + "imageEncoderModelId": "ID du modèle d'encodeur d'image", + "path": "Chemin sur le disque", + "repoVariant": "Variante de dépôt", + "scanResults": "Résultats de l'analyse", + "starterModels": "Modèles de démarrage", + "huggingFace": "HuggingFace", + "metadata": "Métadonnées", + "scanFolder": "Scanner le dossier", + "inplaceInstallDesc": "Installez les modèles sans copier les fichiers. Lors de l'utilisation du modèle, il sera chargé depuis cet emplacement. Si cette option est désactivée, le(s) fichier(s) du modèle seront copiés dans le répertoire des modèles géré par Invoke lors de l'installation.", + "installQueue": "File d'attente d'installation", + "modelImageDeleteFailed": "Échec de la suppression de l'image du modèle", + "modelName": "Nom du modèle", + "triggerPhrases": "Phrases de déclenchement", + "defaultSettings": "Paramètres par défaut", + "simpleModelPlaceholder": "URL ou chemin vers un fichier local ou un dossier de diffuseurs", + "textualInversions": "Inversions textuelles", + "inplaceInstall": "Installation sur place", + "huggingFacePlaceholder": "propriétaire/nom-modèle", + "installRepo": "Installer le dépôt", + "noModelsInstalled": "Aucun modèle installé", + "urlOrLocalPath": "URL ou chemin local", + "prune": "Vider", + "uploadImage": "Importer une image", + "addModels": "Ajouter des modèles", + "install": "Installer", + "localOnly": "local uniquement", + "source": "Source", + "installAll": "Installer tout", + "deleteModelImage": "Supprimer l'image du modèle", + "huggingFaceRepoID": "ID de dépôt HuggingFace", + "loraModels": "LoRAs", + "main": "Principal", + "urlOrLocalPathHelper": "Les URL doivent pointer vers un seul fichier. Les chemins locaux peuvent pointer vers un seul fichier ou un dossier pour un seul modèle de diffuseurs.", + "modelImageUpdateFailed": "Mise à jour de l'image du modèle échouée", + "loraTriggerPhrases": "Phrases de déclenchement LoRA", + "mainModelTriggerPhrases": "Phrases de déclenchement du modèle principal", + "scanPlaceholder": "Chemin vers un dossier local", + "modelImageDeleted": "Image du modèle supprimée", + "upcastAttention": "Augmenter l'Attention", + "noMatchingModels": "Aucun modèle correspondant", + "noModelsInstalledDesc1": "Installer des modèles avec le", + "modelSettings": "Paramètres du modèle", + "edit": "Modifier", + "pruneTooltip": "Vider les importations terminées de la file d'attente", + "pathToConfig": "Chemin vers la configuration", + "modelImageUpdated": "Image du modèle mise à jour", + "scanFolderHelper": "Le dossier sera analysé de manière récursive à la recherche de modèles. Cela peut prendre quelques instants pour des dossiers très volumineux.", + "clipEmbed": "Intégration CLIP", + "spandrelImageToImage": "Image vers Image (Spandrel)", + "t5Encoder": "Encodeur T5", + "learnMoreAboutSupportedModels": "En savoir plus sur les modèles que nous prenons en charge", + "includesNModels": "Contient {{n}} modèles et leurs dépendances", + "starterBundles": "Packs de démarrages", + "starterBundleHelpText": "Installe facilement tous les modèles nécessaire pour démarrer avec un modèle de base, incluant un modèle principal, ControlNets, IP Adapters et plus encore. Choisir un pack igniorera tous les modèles déjà installés.", + "installingXModels_one": "En cours d'installation de {{count}} modèle", + "installingXModels_many": "En cours d'installation de {{count}} modèles", + "installingXModels_other": "En cours d'installation de {{count}} modèles", + "skippingXDuplicates_one": ", en ignorant {{count}} doublon", + "skippingXDuplicates_many": ", en ignorant {{count}} doublons", + "skippingXDuplicates_other": ", en ignorant {{count}} doublons", + "installingModel": "Modèle en cours d'installation", + "installingBundle": "Pack en cours d'installation", + "noDefaultSettings": "Aucun paramètre par défaut configuré pour ce modèle. Visitez le Gestionnaire de Modèles pour ajouter des paramètres par défaut.", + "usingDefaultSettings": "Utilisation des paramètres par défaut du modèle", + "defaultSettingsOutOfSync": "Certain paramètres ne correspondent pas aux valeurs par défaut du modèle :", + "restoreDefaultSettings": "Cliquez pour utiliser les paramètres par défaut du modèle.", + "hfForbiddenErrorMessage": "Nous vous recommandons de visiter la page du modèle. Le propriétaire peut exiger l'acceptation des conditions pour pouvoir télécharger.", + "hfTokenRequired": "Vous essayez de télécharger un modèle qui nécessite un token HuggingFace valide.", + "clipLEmbed": "CLIP-L Embed", + "hfTokenSaved": "Token HF enregistré", + "hfTokenUnableToVerifyErrorMessage": "Impossible de vérifier le token HuggingFace. Cela est probablement dû à une erreur réseau. Veuillez réessayer plus tard.", + "clipGEmbed": "CLIP-G Embed", + "hfTokenUnableToVerify": "Impossible de vérifier le token HF", + "hfTokenInvalidErrorMessage": "Token HuggingFace invalide ou manquant.", + "hfTokenLabel": "Token HuggingFace (Requis pour certains modèles)", + "hfTokenHelperText": "Un token HF est requis pour utiliser certains modèles. Cliquez ici pour créer ou obtenir votre token.", + "hfTokenInvalid": "Token HF invalide ou manquant", + "hfForbidden": "Vous n'avez pas accès à ce modèle HF.", + "hfTokenInvalidErrorMessage2": "Mettre à jour dans le ", + "controlLora": "Controle LoRA", + "urlUnauthorizedErrorMessage2": "Découvrir comment ici.", + "urlUnauthorizedErrorMessage": "Vous devrez peut-être configurer un jeton API pour accéder à ce modèle.", + "urlForbidden": "Vous n'avez pas accès à ce modèle", + "urlForbiddenErrorMessage": "Vous devrez peut-être demander l'autorisation du site qui distribue le modèle." + }, + "parameters": { + "images": "Images", + "steps": "Étapes", + "cfgScale": "Échelle CFG", + "width": "Largeur", + "height": "Hauteur", + "seed": "Graine", + "shuffle": "Nouvelle graine", + "noiseThreshold": "Seuil de Bruit", + "perlinNoise": "Bruit de Perlin", + "type": "Type", + "strength": "Force", + "upscaling": "Agrandissement", + "scale": "Échelle", + "imageFit": "Ajuster Image Initiale à la Taille de Sortie", + "scaleBeforeProcessing": "Échelle Avant Traitement", + "scaledWidth": "Larg. Échelle", + "scaledHeight": "Haut. Échelle", + "infillMethod": "Méthode de Remplissage", + "tileSize": "Taille des Tuiles", + "copyImage": "Copier Image", + "usePrompt": "Utiliser la suggestion", + "useSeed": "Utiliser la graine", + "useAll": "Tout utiliser", + "info": "Info", + "invoke": { + "noPrompts": "Aucun prompts généré", + "missingInputForField": "entrée manquante", + "missingFieldTemplate": "Modèle de champ manquant", + "invoke": "Invoke", + "addingImagesTo": "Ajouter des images à", + "missingNodeTemplate": "Modèle de nœud manquant", + "noModelSelected": "Aucun modèle sélectionné", + "noNodesInGraph": "Aucun nœud dans le graphique", + "systemDisconnected": "Système déconnecté", + "noFLUXVAEModelSelected": "Aucun modèle VAE sélectionné pour la génération FLUX", + "canvasIsTransforming": "La Toile est occupée (en transformation)", + "canvasIsRasterizing": "La Toile est occupée (en rastérisation)", + "noCLIPEmbedModelSelected": "Aucun modèle CLIP Embed sélectionné pour la génération FLUX", + "canvasIsFiltering": "La Toile est occupée (en filtration)", + "noT5EncoderModelSelected": "Aucun modèle T5 Encoder sélectionné pour la génération FLUX", + "canvasIsCompositing": "La Toile est occupée (en composition)", + "collectionTooFewItems": "trop peu d'éléments, minimum {{minItems}}", + "collectionTooManyItems": "trop d'éléments, maximum {{maxItems}}", + "canvasIsSelectingObject": "La toile est occupée (sélection d'objet)", + "batchNodeNotConnected": "Noeud de lots non connecté : {{label}}", + "fluxModelMultipleControlLoRAs": "Vous ne pouvez utiliser qu'un seul Control LoRA à la fois", + "collectionNumberLTMin": "{{value}} < {{minimum}} (incl. min)", + "collectionNumberGTMax": "{{value}} > {{maximum}} (incl. max)", + "collectionNumberGTExclusiveMax": "{{value}} >= {{exclusiveMaximum}} (max exc)", + "batchNodeEmptyCollection": "Certains nœuds de lot ont des collections vides", + "batchNodeCollectionSizeMismatch": "Non-concordance de taille de collection sur le lot {{batchGroupId}}", + "collectionStringTooLong": "trop long, max {{maxLength}}", + "collectionNumberNotMultipleOf": "{{value}} n'est pas un multiple de {{multipleOf}}", + "collectionEmpty": "collection vide", + "collectionStringTooShort": "trop court, min {{minLength}}", + "collectionNumberLTExclusiveMin": "{{value}} <= {{exclusiveMinimum}} (min exc)", + "batchNodeCollectionSizeMismatchNoGroupId": "Taille de collection de groupe par lot non conforme" + }, + "negativePromptPlaceholder": "Prompt Négatif", + "positivePromptPlaceholder": "Prompt Positif", + "general": "Général", + "symmetry": "Symétrie", + "denoisingStrength": "Force de débruitage", + "scheduler": "Planificateur", + "clipSkip": "CLIP Skip", + "seamlessXAxis": "Axe X sans jointure", + "seamlessYAxis": "Axe Y sans jointure", + "controlNetControlMode": "Mode de Contrôle", + "patchmatchDownScaleSize": "Réduire", + "coherenceMode": "Mode", + "maskBlur": "Flou de masque", + "iterations": "Itérations", + "cancel": { + "cancel": "Annuler" + }, + "useCpuNoise": "Utiliser le bruit du CPU", + "imageActions": "Actions d'image", + "setToOptimalSize": "Optimiser la taille pour le modèle", + "setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (peut être trop petit)", + "swapDimensions": "Échanger les dimensions", + "aspect": "Aspect", + "cfgRescaleMultiplier": "Multiplicateur de mise à l'échelle CFG", + "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (peut être trop grand)", + "useSize": "Utiliser la taille", + "remixImage": "Remixer l'image", + "lockAspectRatio": "Verrouiller le rapport hauteur/largeur", + "coherenceEdgeSize": "Taille du bord", + "infillColorValue": "Couleur de remplissage", + "coherenceMinDenoise": "Débruitage minimum", + "sendToCanvas": "Envoyer à la Toile", + "gaussianBlur": "Flou gaussien", + "boxBlur": "Flou de boîte", + "staged": "Mis en attente", + "optimizedImageToImage": "Image vers Image Optimisé", + "sendToUpscale": "Envoyer à Agrandir", + "guidance": "Guidage", + "postProcessing": "Post-traitement (Maj + U)", + "processImage": "Traiter l'image", + "disabledNoRasterContent": "Désactivé (Aucun contenu raster)", + "recallMetadata": "Rappeler les métadonnées" + }, + "settings": { + "models": "Modèles", + "displayInProgress": "Afficher les images progressivement", + "confirmOnDelete": "Confirmer la suppression", + "resetWebUI": "Réinitialiser l'interface Web", + "resetWebUIDesc1": "Réinitialiser l'interface Web ne réinitialise que le cache local du navigateur de vos images et de vos paramètres enregistrés. Cela n'efface pas les images du disque.", + "resetWebUIDesc2": "Si les images ne s'affichent pas dans la galerie ou si quelque chose d'autre ne fonctionne pas, veuillez essayer de réinitialiser avant de soumettre une demande sur GitHub.", + "resetComplete": "L'interface Web a été réinitialisée.", + "general": "Général", + "showProgressInViewer": "Afficher les images progressivement dans le Visualiseur", + "antialiasProgressImages": "Anti Alisasing des Images progressives", + "beta": "Bêta", + "generation": "Génération", + "ui": "Interface Utilisateur", + "developer": "Développeur", + "enableNSFWChecker": "Activer le vérificateur NSFW", + "clearIntermediatesDesc2": "Les images intermédiaires sont des sous-produits de la génération, différentes des images de résultat dans la galerie. La suppression des intermédiaires libérera de l'espace disque.", + "clearIntermediatesDisabled": "La file d'attente doit être vide pour effacer les intermédiaires", + "reloadingIn": "Rechargement dans", + "intermediatesClearedFailed": "Problème de suppression des intermédiaires", + "clearIntermediates": "Effacer les intermédiaires", + "enableInvisibleWatermark": "Activer le Filigrane Invisible", + "clearIntermediatesDesc1": "Effacer les intermédiaires réinitialisera votre Toile et votre ControlNet.", + "enableInformationalPopovers": "Activer les infobulles d'information", + "intermediatesCleared_one": "Effacé {{count}} Intermédiaire", + "intermediatesCleared_many": "Effacé {{count}} Intermédiaires", + "intermediatesCleared_other": "Effacé {{count}} Intermédiaires", + "clearIntermediatesDesc3": "Vos images de galerie ne seront pas supprimées.", + "clearIntermediatesWithCount_one": "Effacé {{count}} Intermédiaire", + "clearIntermediatesWithCount_many": "Effacé {{count}} Intermédiaires", + "clearIntermediatesWithCount_other": "Effacé {{count}} Intermédiaires", + "informationalPopoversDisabled": "Pop-ups d'information désactivés", + "informationalPopoversDisabledDesc": "Les pop-ups d'information ont été désactivés. Activez-les dans les paramètres.", + "confirmOnNewSession": "Confirmer lors d'une nouvelle session", + "enableModelDescriptions": "Activer les descriptions de modèle dans les menus déroulants", + "showDetailedInvocationProgress": "Afficher les détails de progression" + }, + "toast": { + "uploadFailed": "Importation échouée", + "imageCopied": "Image copiée", + "parametersNotSet": "Paramètres non rappelés", + "serverError": "Erreur du serveur", + "uploadFailedInvalidUploadDesc": "Doit être des images au format PNG ou JPEG.", + "problemCopyingImage": "Impossible de copier l'image", + "parameterSet": "Paramètre Rappelé", + "parameterNotSet": "Paramètre non Rappelé", + "canceled": "Traitement annulé", + "addedToBoard": "Ajouté aux ressources de la planche {{name}}", + "workflowLoaded": "Workflow chargé", + "connected": "Connecté au serveur", + "imageUploadFailed": "Échec de l'importation de l'image", + "loadedWithWarnings": "Workflow chargé avec des avertissements", + "imageUploaded": "Image importée", + "modelAddedSimple": "Modèle ajouté à la file d'attente", + "workflowDeleted": "Workflow supprimé", + "baseModelChangedCleared_one": "Effacé ou désactivé {{count}} sous-modèle incompatible", + "baseModelChangedCleared_many": "Effacé ou désactivé {{count}} sous-modèles incompatibles", + "baseModelChangedCleared_other": "Effacé ou désactivé {{count}} sous-modèles incompatibles", + "problemDownloadingImage": "Impossible de télécharger l'image", + "problemRetrievingWorkflow": "Problème de récupération du Workflow", + "problemDeletingWorkflow": "Problème de suppression du Workflow", + "prunedQueue": "File d'attente vidée", + "parameters": "Paramètres", + "modelImportCanceled": "Importation du modèle annulée", + "sentToCanvas": "Envoyé à la Toile", + "sentToUpscale": "Envoyé à l'Agrandissement", + "unableToLoadImage": "Impossible de charger l'image", + "unableToLoadImageMetadata": "Impossible de charger les métadonnées de l'image", + "errorCopied": "Erreur copiée", + "parametersSet": "Paramètres rappelés", + "somethingWentWrong": "Quelque chose a échoué", + "unableToLoadStylePreset": "Impossible de charger le préréglage de style", + "stylePresetLoaded": "Préréglage de style chargé", + "parameterNotSetDescWithMessage": "Impossible de rappeler {{parameter}} : {{message}}", + "importFailed": "Importation échouée", + "importSuccessful": "Importation réussie", + "outOfMemoryError": "Erreur de mémoire insuffisante", + "sessionRef": "Session : {{sessionId}}", + "outOfMemoryErrorDesc": "Vos paramètres de génération actuels dépassent la capacité du système. Veuillez ajuster vos paramètres et réessayer.", + "parameterSetDesc": "Rappelé {{parameter}}", + "parameterNotSetDesc": "Impossible de rappeler {{parameter}}", + "layerCopiedToClipboard": "Calque copié dans le presse-papiers", + "problemCopyingLayer": "Impossible de copier la couche", + "baseModelChanged": "Modèle de base changé", + "problemSavingLayer": "Impossible d'enregistrer la couche", + "linkCopied": "Lien copié", + "imagesWillBeAddedTo": "Les images Importées seront ajoutées au ressources de la Planche {{boardName}}.", + "addedToUncategorized": "Ajouté aux ressources de la planche $t(boards.uncategorized)", + "pasteSuccess": "Collé à {{destination}}", + "pasteFailed": "Échec du collage", + "outOfMemoryErrorDescLocal": "Suivez notre guide Low VRAM pour réduire les OOMs.", + "unableToCopy": "Incapable de Copier", + "unableToCopyDesc": "Votre navigateur ne prend pas en charge l'accès au presse-papiers. Les utilisateurs de Firefox peuvent peut-être résoudre ce problème en suivant ", + "unableToCopyDesc_theseSteps": "ces étapes" + }, + "accessibility": { + "uploadImage": "Importer une image", + "reset": "Réinitialiser", + "nextImage": "Image suivante", + "previousImage": "Image précédente", + "invokeProgressBar": "Barre de Progression Invoke", + "menu": "Menu", + "about": "À propos", + "mode": "Mode", + "createIssue": "Créer un ticket", + "submitSupportTicket": "Envoyer un ticket de support", + "resetUI": "$t(accessibility.reset) l'Interface Utilisateur", + "toggleRightPanel": "Afficher/Masquer le panneau de droite (G)", + "toggleLeftPanel": "Afficher/Masquer le panneau de gauche (T)", + "uploadImages": "Importer Image(s)" + }, + "boards": { + "move": "Déplacer", + "cancel": "Annuler", + "loading": "Chargement…", + "archived": "Archivé", + "clearSearch": "Effacer la recherche", + "imagesWithCount_one": "{{count}} image", + "imagesWithCount_many": "{{count}} images", + "imagesWithCount_other": "{{count}} images", + "bottomMessage": "Supprimer cette planche et ses images va réinitialiser toutes les fonctionnalités les utilisant.", + "deleteBoardAndImages": "Supprimer la Planche et les Images", + "deleteBoardOnly": "Supprimer la Planche uniquement", + "assetsWithCount_one": "{{count}} ressource", + "assetsWithCount_many": "{{count}} ressources", + "assetsWithCount_other": "{{count}} ressources", + "selectedForAutoAdd": "Séléctioné pour Ajout Automatique", + "noMatching": "Pas de Planches correspondantes", + "myBoard": "Ma Planche", + "menuItemAutoAdd": "Ajouter automatiquement à cette Planche", + "changeBoard": "Changer de Planche", + "movingImagesToBoard_one": "Déplacer {{count}} image à cette planche :", + "movingImagesToBoard_many": "Déplacer {{count}} images à cette planche :", + "movingImagesToBoard_other": "Déplacer {{count}} image à cette planche :", + "noBoards": "Pas de Planches {{boardType}}", + "shared": "Planches Partagées", + "searchBoard": "Chercher les Planches...", + "addSharedBoard": "Créer une Planche Partagée", + "addPrivateBoard": "Créer une Planche Privée", + "boards": "Planches", + "deletedPrivateBoardsCannotbeRestored": "Les planches supprimées ne peuvent pas être restaurées. Séléctionner 'Supprimer la planche uniquement' placera les images dans un état non catégorisé pour le créateur des images.", + "uncategorized": "Non catégorisé", + "downloadBoard": "Télécharger la Planche", + "private": "Planches Privées", + "deleteBoard": "Supprimer la Planche", + "autoAddBoard": "Création de Planche Automatique", + "addBoard": "Créer une Planche", + "topMessage": "Cette planche contient des images utilisée dans ces fonctionnalités :", + "selectBoard": "Séléctionner une Planche", + "archiveBoard": "Archiver la Planche", + "unarchiveBoard": "Déarchiver la Planche", + "deletedBoardsCannotbeRestored": "Les planches supprimées ne peuvent pas être restaurées. Séléctionner 'Supprimer la planche uniquement' placera les images dans un état non catégorisé.", + "updateBoardError": "Erreur de mise à jour de la planche" + }, + "accordions": { + "advanced": { + "title": "Avancé", + "options": "Options $t(accordions.advanced.title)" + }, + "image": { + "title": "Image" + }, + "compositing": { + "title": "Composition", + "coherenceTab": "Passe de Cohérence", + "infillTab": "Remplissage" + }, + "generation": { + "title": "Génération" + }, + "control": { + "title": "Controle" + } + }, + "queue": { + "clear": "Effacer", + "failed": "Échec", + "session": "Session", + "queueEmpty": "File d'attente vide", + "next": "Suivant", + "queue": "File d'attente", + "clearSucceeded": "File d'attente effacée", + "total": "Total", + "pending": "En attente", + "in_progress": "En cours", + "time": "Heure", + "status": "État", + "openQueue": "Ouvrir la file d'attente", + "queueFront": "Ajouter en premier", + "cancel": "Annuler", + "canceled": "Annulé", + "clearQueueAlertDialog2": "Voulez-vous vraiment effacer la file d'attente ?", + "queueBack": "Ajouter à la file d'attente", + "completed": "Terminé", + "pauseSucceeded": "Traitement intérompu", + "cancelBatchFailed": "Problème lors de l'annulation du Lot", + "resumeTooltip": "Reprendre le traitement", + "resumeFailed": "Problème lors de la reprise du traitement", + "cancelItem": "Annuler l'élément", + "pruneSucceeded": "Purgé {{item_count}} éléments complété de la file d'attente", + "cancelTooltip": "Annuler l'élément actuel", + "current": "Actuel", + "pause": "Pause", + "clearTooltip": "Annuler et Effacer tous les éléments", + "pauseFailed": "Problème lors de l'intéruption du traitement", + "cancelBatch": "Annuler le Lot", + "pauseTooltip": "Intérrompre le traitement", + "prune": "Purger", + "pruneFailed": "Problème lors du Purgeage de la file d'attente", + "clearQueueAlertDialog": "Effacer la file d'attente immédiatement annule tous les éléments en cours de traitement et efface entièrement la file d'attente. Les filtres en attente seront également annulés.", + "pruneTooltip": "Purger {{item_count}} élémentscomplétés", + "cancelSucceeded": "Élément annulé", + "cancelFailed": "Problème lors de l'annulation de l'élément", + "clearFailed": "Problème lors de l'Effacement de la file d'attente", + "cancelBatchSucceeded": "Lot Annulé", + "resume": "Reprendre", + "resumeSucceeded": "Traitement repris", + "enqueueing": "Ajout du Lot à la file d'attente", + "origin": "Origine", + "destination": "Destination", + "batch": "Lot", + "completedIn": "Complété en", + "upscaling": "Agrandissement", + "canvas": "Toile", + "batchQueuedDesc_one": "Ajouté {{count}} session à {{direction}} de la file d'attente", + "batchQueuedDesc_many": "Ajouté {{count}} sessions à {{direction}} de la file d'attente", + "batchQueuedDesc_other": "Ajouté {{count}} sessions à {{direction}} de la file d'attente", + "prompts_one": "Prompt", + "prompts_many": "Prompts", + "prompts_other": "Prompts", + "batchQueued": "Lot ajouté à la file d'attente", + "gallery": "Galerie", + "notReady": "Impossible d'ajouter à la file d'attente", + "front": "début", + "graphQueued": "Graph ajouté à la file d'attente", + "other": "Autre", + "generation": "Génération", + "workflows": "Workflows", + "batchFailedToQueue": "Impossible d'ajouter le Lot dans à la file d'attente", + "graphFailedToQueue": "Impossible d'ajouter le graph à la file d'attente", + "item": "Élément", + "generations_one": "Génération", + "generations_many": "Générations", + "generations_other": "Générations", + "iterations_one": "Itération", + "iterations_many": "Itérations", + "iterations_other": "Itérations", + "back": "fin", + "batchSize": "Taille de lot", + "retryFailed": "Problème de nouvelle tentative de l'élément", + "retrySucceeded": "Élément Retenté", + "retryItem": "Réessayer l'élement", + "cancelAllExceptCurrentQueueItemAlertDialog": "Annuler tous les éléments de la file d'attente, sauf celui en cours, arrêtera les éléments en attente mais permettra à celui en cours de se terminer.", + "cancelAllExceptCurrentQueueItemAlertDialog2": "Êtes-vous sûr de vouloir annuler tous les éléments en attente dans la file d'attente ?", + "cancelAllExceptCurrentTooltip": "Annuler tout sauf l'élément actuel", + "confirm": "Confirmer" + }, + "prompt": { + "noMatchingTriggers": "Pas de déclancheurs correspondants", + "addPromptTrigger": "Ajouter un déclencheur de Prompt", + "compatibleEmbeddings": "Embeddings Compatibles" + }, + "hrf": { + "metadata": { + "enabled": "Correction Haute Résolution Activée", + "strength": "Force de la Correction Haute Résolution", + "method": "Méthode de la Correction Haute Résolution" + }, + "hrf": "Correction Haute Résolution" + }, + "invocationCache": { + "clear": "Vider", + "useCache": "Utiliser le Cache", + "invocationCache": "Cache des Invocations", + "enableFailed": "Problème lors de l'activation du Cache d'Invocation", + "enable": "Activer", + "enableSucceeded": "Cache d'Invocation Activé", + "clearSucceeded": "Cache d'Invocation vidé", + "disable": "Désactiver", + "disableSucceeded": "Cache d'Invocation désactivé", + "maxCacheSize": "Taille du Cache maximum", + "misses": "Non trouvé dans le Cache", + "clearFailed": "Problème lors du vidage du Cache d'Invocation", + "cacheSize": "Taille du Cache", + "hits": "Trouvé dans le Cache", + "disableFailed": "Problème lors de la désactivation du Cache d'Invocation" + }, + "hotkeys": { + "hotkeys": "Raccourci clavier", + "viewer": { + "recallPrompts": { + "desc": "Rappeler le prompt positif et négatif pour l'image actuelle.", + "title": "Rappeler les Prompts" + }, + "nextComparisonMode": { + "desc": "Faire défiler les modes de comparaison.", + "title": "Mode de comparaison suivant" + }, + "runPostprocessing": { + "title": "Exécuter le post-traitement", + "desc": "Exécute le post-traitement sélectionné sur l'image actuelle." + }, + "toggleViewer": { + "title": "Afficher/Masquer le visualiseur d'images", + "desc": "Afficher ou masquer le visualiseur d'images. Disponible uniquement dans l'onglet Toile." + }, + "swapImages": { + "title": "Échanger les images de comparaison", + "desc": "Échange les images comparées." + }, + "title": "Visualiseur d'images", + "recallAll": { + "title": "Rappeler toutes les métadonnées", + "desc": "Rappelle toutes les métadonnées pour l'image actuelle." + }, + "loadWorkflow": { + "title": "Ouvrir un Workflow", + "desc": "Charge le workflow enregistré lié à l'image actuelle (s'il en a un)." + }, + "recallSeed": { + "desc": "Rappelle la graine pour l'image actuelle.", + "title": "Rappeler la graine" + }, + "useSize": { + "title": "Utiliser la taille", + "desc": "Utilisez la taille de l'image actuelle comme taille de la bounding box." + }, + "toggleMetadata": { + "title": "Afficher/Masquer les métadonnées", + "desc": "Affiche ou masque la superposition des métadonnées de l'image actuelle." + }, + "remix": { + "title": "Remixer", + "desc": "Rappelle toutes les métadonnées sauf la graine pour l'image actuelle." + } + }, + "searchHotkeys": "Recherche raccourci clavier", + "app": { + "selectQueueTab": { + "desc": "Selectionne l'onglet de file d'attente.", + "title": "Sélectionner l'onglet File d'Attente" + }, + "title": "Application", + "invoke": { + "title": "Invoke", + "desc": "Ajouter une génération à la fin de la file d'attente." + }, + "invokeFront": { + "title": "Invoke (Front)", + "desc": "Ajouter une génération au début de la file d'attente." + }, + "cancelQueueItem": { + "title": "Annuler", + "desc": "Annuler l'élément en cours de traitement dans la file d'attente." + }, + "clearQueue": { + "title": "Vider la file d'attente", + "desc": "Annuler et retirer tous les éléments de la file d'attente." + }, + "selectCanvasTab": { + "title": "Séléctionner l'onglet Toile", + "desc": "Séléctionne l'onglet Toile." + }, + "selectUpscalingTab": { + "title": "Séléctionner l'onglet Agrandissement", + "desc": "Séléctionne l'onglet Agrandissement." + }, + "selectWorkflowsTab": { + "desc": "Sélectionne l'onglet Workflows.", + "title": "Sélectionner l'onglet Workflows" + }, + "togglePanels": { + "desc": "Affiche ou masque les panneaux gauche et droit en même temps.", + "title": "Afficher/Masquer les panneaux" + }, + "selectModelsTab": { + "desc": "Sélectionne l'onglet Modèles.", + "title": "Sélectionner l'onglet Modèles" + }, + "focusPrompt": { + "title": "Selectionne le Prompt", + "desc": "Déplace le focus du curseur sur le prompt positif." + }, + "toggleLeftPanel": { + "title": "Afficher/Masquer le panneau de gauche", + "desc": "Affiche ou masque le panneau de gauche." + }, + "resetPanelLayout": { + "desc": "Réinitialise les panneaux gauche et droit à leur taille et disposition par défaut.", + "title": "Reinitialiser l'organisation des panneau" + }, + "toggleRightPanel": { + "title": "Afficher/Masquer le panneau de droite", + "desc": "Affiche ou masque le panneau de droite." + } + }, + "canvas": { + "title": "Toile", + "selectBrushTool": { + "title": "Outil Pinceau", + "desc": "Sélectionne l'outil pinceau." + }, + "incrementToolWidth": { + "title": "Augmenter largeur de l'outil", + "desc": "Augmente la largeur du pinceau ou de la gomme, en fonction de la sélection." + }, + "selectColorPickerTool": { + "title": "Outil Pipette", + "desc": "Sélectionne l'outil pipette pour la sélection de couleur." + }, + "selectEraserTool": { + "title": "Outil Gomme", + "desc": "Sélectionne l'outil gomme." + }, + "selectMoveTool": { + "title": "Outil Déplacer", + "desc": "Sélectionne l'outil déplacer." + }, + "selectRectTool": { + "title": "Outil Rectangle", + "desc": "Sélectionne l'outil rectangle." + }, + "selectViewTool": { + "title": "Outil Visualisation", + "desc": "Sélectionne l'outil visualisation." + }, + "selectBboxTool": { + "title": "Outil Cadre de délimitation", + "desc": "Sélectionne l'outil cadre de délimitation." + }, + "fitLayersToCanvas": { + "title": "Adapte les Couches à la Toile", + "desc": "Mettre à l'échelle et positionner la vue pour l'adapter à tous les couches visibles." + }, + "fitBboxToCanvas": { + "desc": "Ajuster l'échelle et la position de la vue pour s'adapter au cadre de délimitation.", + "title": "Ajuster le cadre de délimitation à la Toile" + }, + "decrementToolWidth": { + "title": "Réduire largeur de l'outil", + "desc": "Réduit la largeur du pinceau ou de la gomme, en fonction de la sélection." + }, + "setZoomTo800Percent": { + "title": "Zoomer à 800 %", + "desc": "Définit le zoom de la toile à 800 %." + }, + "setZoomTo400Percent": { + "desc": "Définit le zoom de la toile à 400 %.", + "title": "Zoomer à 400 %" + }, + "transformSelected": { + "title": "Transformer", + "desc": "Transforme la couche sélectionnée." + }, + "quickSwitch": { + "title": "Commutateur rapide de couche", + "desc": "Alterner entre les deux dernières couches sélectionnées. Si une couche est marquée, alternez toujours entre celle-ci et la dernière couche non marquée." + }, + "setZoomTo200Percent": { + "desc": "Définit le zoom de la toile à 200 %.", + "title": "Zoomer à 200 %" + }, + "filterSelected": { + "title": "Filtrer", + "desc": "Filtre la couche sélectionnée. S'applique uniquement aux couches de rastérisation et de contrôle." + }, + "setZoomTo100Percent": { + "title": "Zoomer à 100 %", + "desc": "Définir le zoom de la toile à 100 %." + }, + "cancelTransform": { + "desc": "Annule la transformation en attente.", + "title": "Annuler la transformation" + }, + "applyTransform": { + "desc": "Applique la transformation en attente à la couche sélectionnée.", + "title": "Appliquer la transformation" + }, + "cancelFilter": { + "title": "Annuler le filtre", + "desc": "Annule le filtre en attente." + }, + "applyFilter": { + "title": "Appliquer le filtre", + "desc": "Applique le filtre en attente à la couche sélectionnée." + }, + "deleteSelected": { + "title": "Supprimer la couche", + "desc": "Supprime la couche sélectionnée." + }, + "resetSelected": { + "title": "Réinitialiser la couche", + "desc": "Réinitialiser la couche sélectionnée. S'applique uniquement au masque de retouche et au guidage régional." + }, + "undo": { + "title": "Annuler", + "desc": "Annule la dernière action sur la toile." + }, + "nextEntity": { + "desc": "Sélectionne la couche suivante dans la liste.", + "title": "Couche suivante" + }, + "redo": { + "title": "Rétablir", + "desc": "Rétablir la dernière action sur la toile." + }, + "prevEntity": { + "title": "Couche Précédente", + "desc": "Sélectionne la couche précédente dans la liste." + } + }, + "clearSearch": "Annuler la recherche", + "noHotkeysFound": "Aucun raccourci clavier trouvé", + "gallery": { + "deleteSelection": { + "desc": "Supprime toutes les images séléctionnées. Par défault une confirmation vous sera demandée. Si les images sont actuellement utilisées dans l'application vous serez mis en garde.", + "title": "Supprimer" + }, + "galleryNavRightAlt": { + "title": "Naviguer à droite (Comparaison d'Image)", + "desc": "Identique à Naviguer à droite, mais sélectionne l'image de comparaison, ouvrant le mode de comparaison s'il n'est pas déjà ouvert." + }, + "galleryNavUpAlt": { + "desc": "Identique à \"Naviguer vers le haut\", mais sélectionne l'image de comparaison, ouvrant le mode de comparaison s'il n'est pas déjà ouvert.", + "title": "Naviguer vers le haut (Comparaison d'Image)" + }, + "galleryNavDownAlt": { + "title": "Naviguer vers le bas (Comparaison d'Image)", + "desc": "Identique à Naviguer vers le bas, mais sélectionne l'image de comparaison, ouvrant le mode de comparaison s'il n'est pas déjà ouvert." + }, + "galleryNavRight": { + "title": "Naviguer à droite", + "desc": "Navigue vers la droite dans la grille de la galerie, en sélectionnant cette image. Si vous êtes à la dernière image de la ligne, passez à la ligne suivante. Si vous êtes à la dernière image de la page, passe à la page suivante." + }, + "selectAllOnPage": { + "desc": "Sélectionne toutes les images sur la page actuelle.", + "title": "Sélectionner tout sur la page" + }, + "clearSelection": { + "title": "Effacer la sélection", + "desc": "Efface la sélection actuelle, le cas échéant." + }, + "galleryNavLeft": { + "title": "Naviguer à gauche", + "desc": "Navigue vers la gauche dans la grille de la galerie, en sélectionnant cette image. Si vous êtes à la première image de la ligne, allez à la ligne précédente. Si vous êtes à la première image de la page, va à la page précédente." + }, + "galleryNavDown": { + "desc": "Navigue vers le bas dans la grille de la galerie, en sélectionnant cette image. Si vous êtes en bas de la page, va à la page suivante.", + "title": "Naviguer vers le bas" + }, + "galleryNavLeftAlt": { + "title": "Naviguer à gauche (Comparaison d'Image)", + "desc": "Identique à Naviguer à gauche, mais sélectionne l'image de comparaison, ouvrant le mode de comparaison s'il n'est pas déjà ouvert." + }, + "title": "Galerie", + "galleryNavUp": { + "title": "Naviguer vers le haut", + "desc": "Navigue vers le haut dans la grille de la galerie, en sélectionnant cette image. Si vous êtes en haut de la page, va à la page précédente." + } + }, + "workflows": { + "selectAll": { + "title": "Sélectionner tout", + "desc": "Sélectionne tous les nœuds et connexions." + }, + "deleteSelection": { + "title": "Supprimer", + "desc": "Supprime les nœuds et les connexions sélectionnés." + }, + "undo": { + "title": "Annuler", + "desc": "Annule la dernière action de workflow." + }, + "redo": { + "title": "Rétablir", + "desc": "Rétablit la dernière action de workflow." + }, + "addNode": { + "desc": "Ouvre le menu d'ajout de nœud.", + "title": "Ajouter un nœud" + }, + "pasteSelectionWithEdges": { + "title": "Coller avec connections", + "desc": "Colle les nœuds copiés, les arêtes et toutes les connections des nœuds copiés." + }, + "copySelection": { + "desc": "Copie les nœuds et les connections sélectionnés.", + "title": "Copier" + }, + "pasteSelection": { + "desc": "Colle les nœuds et les connections copiés.", + "title": "Coller" + }, + "title": "Workflows" + } + }, + "popovers": { + "paramPositiveConditioning": { + "paragraphs": [ + "Guide le processus de génération. Vous pouvez utiliser n'importe quels mots ou phrases.", + "Prend en charge les syntaxes et les embeddings de Compel et des Prompts dynamiques." + ], + "heading": "Prompt Positif" + }, + "paramNegativeConditioning": { + "paragraphs": [ + "Le processus de génération évite les concepts dans le prompt négatif. Utilisez cela pour exclure des qualités ou des objets du résultat.", + "Prend en charge la syntaxe et les embeddings de Compel." + ], + "heading": "Prompt Négatif" + }, + "paramVAEPrecision": { + "heading": "Précision du VAE", + "paragraphs": [ + "La précision utilisée lors de l'encodage et du décodage VAE.", + "La pr'ecision Fp16/Half est plus efficace, au détriment de légères variations d'image." + ] + }, + "controlNetWeight": { + "heading": "Poids", + "paragraphs": [ + "Poids du Control Adapter. Un poids plus élevé aura un impact plus important sur l'image finale.", + "• Poids plus élevé (.75-2) : Crée un impact plus significatif sur le résultat final.", + "• Poids inférieur (0-.75) : Crée un impact plus faible sur le résultat final." + ] + }, + "compositingMaskAdjustments": { + "heading": "Ajustements de masque", + "paragraphs": [ + "Ajuste le masque." + ] + }, + "infillMethod": { + "heading": "Méthode de Remplissage", + "paragraphs": [ + "Méthode de remplissage lors du processus d'Outpainting ou d'Inpainting." + ] + }, + "clipSkip": { + "paragraphs": [ + "Combien de couches du modèle CLIP faut-il ignorer.", + "Certains modèles sont mieux adaptés à une utilisation avec CLIP Skip." + ], + "heading": "CLIP Skip" + }, + "paramScheduler": { + "heading": "Planificateur", + "paragraphs": [ + "Planificateur utilisé pendant le processus de génération.", + "Chaque planificateur définit comment ajouter de manière itérative du bruit à une image ou comment mettre à jour un échantillon en fonction de la sortie d'un modèle." + ] + }, + "controlNet": { + "paragraphs": [ + "Les ControlNets fournissent des indications au processus de génération, aidant à créer des images avec une composition, une structure ou un style contrôlés, en fonction du modèle sélectionné." + ], + "heading": "ControlNet" + }, + "paramSteps": { + "heading": "Étapes", + "paragraphs": [ + "Nombre d'étapes qui seront effectuées à chaque génération.", + "Des nombres d'étapes plus élevés créeront généralement de meilleures images, mais nécessiteront plus de temps de génération." + ] + }, + "controlNetBeginEnd": { + "heading": "Pourcentage de début / de fin d'étape", + "paragraphs": [ + "Ce paramètre détérmine quelle portion du processus de débruitage (génération) utilisera cette couche comme guide.", + "En général, les Control Adapter appliqués au début du processus guident la composition, tandis que les Control Adapter appliqués à la fin guident les détails.", + "• Étape de fin (%): Spécifie quand arrêter d'appliquer le guide de cette couche et revenir aux guides généraux du modèle et aux autres paramètres." + ] + }, + "controlNetControlMode": { + "paragraphs": [ + "Accordez plus de poids soit au prompt, soit au ControlNet." + ], + "heading": "Mode de Contrôle" + }, + "dynamicPromptsSeedBehaviour": { + "heading": "Comportement de la graine", + "paragraphs": [ + "Contrôle l'utilisation de la graine lors de la génération des prompts.", + "Une graine unique pour chaque itération. Utilisez ceci pour explorer les variations de prompt sur une seule graine.", + "Par exemple, si vous avez 5 prompts, chaque image utilisera la même graine.", + "Par image utilisera une graine unique pour chaque image. Cela offre plus de variation." + ] + }, + "paramVAE": { + "heading": "VAE", + "paragraphs": [ + "Modèle utilisé pour convertir la sortie de l'IA en l'image finale." + ] + }, + "compositingCoherenceMode": { + "heading": "Mode", + "paragraphs": [ + "Méthode utilisée pour créer une image cohérente avec la zone masquée nouvellement générée." + ] + }, + "paramIterations": { + "heading": "Itérations", + "paragraphs": [ + "Le nombre d'images à générer.", + "Si les prompts dynamiques sont activées, chaque prompt sera généré autant de fois." + ] + }, + "dynamicPrompts": { + "paragraphs": [ + "Les Prompts dynamiques divisent un seul prompt en plusieurs.", + "La syntaxe de base est \"une balle {rouge|verte|bleue}\". Cela produira trois prompts : \"une balle rouge\", \"une balle verte\" et \"une balle bleue\".", + "Vous pouvez utiliser la syntaxe autant de fois que vous le souhaitez dans un seul prompt, mais veillez à garder le nombre de prompts générées sous contrôle avec le paramètre Max Prompts." + ], + "heading": "Prompts Dynamiques" + }, + "paramModel": { + "heading": "Modèle", + "paragraphs": [ + "Modèle utilisé pour la génération. Différents modèles sont entraînés pour se spécialiser dans la production de résultats esthétiques et de contenus variés." + ] + }, + "compositingCoherencePass": { + "heading": "Passe de cohérence", + "paragraphs": [ + "Un deuxième tour de débruitage aide à composer l'image remplie/étendue." + ] + }, + "paramRatio": { + "heading": "Rapport hauteur/largeur", + "paragraphs": [ + "Le rapport hauteur/largeur de l'image générée.", + "Une taille d'image (en nombre de pixels) équivalente à 512x512 est recommandée pour les modèles SD1.5 et une taille équivalente à 1024x1024 est recommandée pour les modèles SDXL." + ] + }, + "paramSeed": { + "heading": "Graine", + "paragraphs": [ + "Contrôle le bruit de départ utilisé pour la génération.", + "Désactivez l'option \"Aléatoire\" pour produire des résultats identiques avec les mêmes paramètres de génération." + ] + }, + "scaleBeforeProcessing": { + "heading": "Échelle avant traitement", + "paragraphs": [ + "\"Auto\" ajuste la zone sélectionnée à la taille la mieux adaptée au modèle avant le processus de génération d'image.", + "\"Manuel\" vous permet de choisir la largeur et la hauteur auxquelles la zone sélectionnée sera redimensionnée avant le processus de génération d'image." + ] + }, + "compositingBlurMethod": { + "heading": "Méthode de flou", + "paragraphs": [ + "La méthode de flou appliquée à la zone masquée." + ] + }, + "controlNetResizeMode": { + "heading": "Mode de Redimensionnement", + "paragraphs": [ + "Méthode pour adapter la taille de l'image d'entrée du Control Adapter à la taille de l'image générée." + ] + }, + "dynamicPromptsMaxPrompts": { + "heading": "Max Prompts", + "paragraphs": [ + "Limite le nombre de prompts pouvant être générés par les Prompts Dynamiques." + ] + }, + "paramDenoisingStrength": { + "heading": "Force de débruitage", + "paragraphs": [ + "Intensité du bruit ajouté à l'image d'entrée.", + "0 produira une image identique, tandis que 1 produira une image complètement différente.", + "Lorsque aucune couche raster avec du contenu visible n'est présente, ce paramètre est ignoré." + ] + }, + "lora": { + "heading": "LoRA", + "paragraphs": [ + "Modèles légers utilisés en conjonction avec des modèles de base." + ] + }, + "noiseUseCPU": { + "heading": "Utiliser le bruit du CPU", + "paragraphs": [ + "Contrôle si le bruit est généré sur le CPU ou le GPU.", + "Avec le bruit du CPU activé, une graine particulière produira la même image sur n'importe quelle machine.", + "Il n'y a aucun impact sur les performances à activer le bruit du CPU." + ] + }, + "paramCFGScale": { + "heading": "Échelle CFG", + "paragraphs": [ + "Contrôle de l'influence du prompt sur le processus de génération.", + "Des valeurs élevées de l'échelle CFG peuvent entraîner une saturation excessive et des distortions. " + ] + }, + "loraWeight": { + "heading": "Poids", + "paragraphs": [ + "Poids du LoRA. Un poids plus élevé aura un impact plus important sur l'image finale." + ] + }, + "imageFit": { + "heading": "Ajuster l'image initiale à la taille de sortie", + "paragraphs": [ + "Redimensionne l'image initiale à la largeur et à la hauteur de l'image de sortie. Il est recommandé de l'activer." + ] + }, + "paramCFGRescaleMultiplier": { + "heading": "Multiplicateur de mise à l'échelle CFG", + "paragraphs": [ + "Multiplicateur de mise à l'échelle pour le guidage CFG, utilisé pour les modèles entraînés en utilisant le zero-terminal SNR (ztsnr).", + "Une valeur de 0.7 est suggérée pour ces modèles." + ] + }, + "controlNetProcessor": { + "heading": "Processeur", + "paragraphs": [ + "Méthode de traitement de l'image d'entrée pour guider le processus de génération. Différents processeurs fourniront différents effets ou styles dans vos images générées." + ] + }, + "paramUpscaleMethod": { + "paragraphs": [ + "Méthode utilisée pour améliorer l'image pour la correction de haute résolution." + ], + "heading": "Méthode d'agrandissement" + }, + "refinerModel": { + "heading": "Modèle de Raffinage", + "paragraphs": [ + "Modèle utilisé pendant la partie raffinage du processus de génération.", + "Similaire au Modèle de Génération." + ] + }, + "paramWidth": { + "paragraphs": [ + "Largeur de l'image générée. Doit être un multiple de 8." + ], + "heading": "Largeur" + }, + "paramHeight": { + "heading": "Hauteur", + "paragraphs": [ + "Hauteur de l'image générée. Doit être un multiple de 8." + ] + }, + "paramHrf": { + "heading": "Activer la correction haute résolution", + "paragraphs": [ + "Générez des images de haute qualité à une résolution plus grande que celle qui est optimale pour le modèle. Cela est généralement utilisé pour prévenir la duplication dans l'image générée." + ] + }, + "patchmatchDownScaleSize": { + "paragraphs": [ + "Intensité du sous-échantillonage qui se produit avant le remplissage.", + "Un sous-échantillonage plus élevé améliorera les performances et réduira la qualité." + ], + "heading": "Sous-échantillonage" + }, + "paramAspect": { + "paragraphs": [ + "Rapport hauteur/largeur de l'image générée. Changer le rapport mettra à jour la largeur et la hauteur en conséquence.", + "\"Optimiser\" définira la largeur et la hauteur aux dimensions optimales pour le modèle choisi." + ], + "heading": "Aspect" + }, + "refinerScheduler": { + "heading": "Planificateur", + "paragraphs": [ + "Planificateur utilisé pendant la partie de raffinage du processus de génération.", + "Semblable au Planificateur de Génération." + ] + }, + "refinerPositiveAestheticScore": { + "paragraphs": [ + "Ajoute un biais envers les générations pour qu'elles soient plus similaires aux images ayant un score esthétique élevé, en fonction des données d'entraînement." + ], + "heading": "Score Esthétique Positif" + }, + "refinerNegativeAestheticScore": { + "heading": "Score Esthétique Négatif", + "paragraphs": [ + "Ajoute un biais envers les générations pour qu'elles soient plus similaires aux images ayant un faible score esthétique, en fonction des données d'entraînement." + ] + }, + "seamlessTilingYAxis": { + "paragraphs": [ + "Concaténer une image sans bord le long de l'axe vertical." + ], + "heading": "Concaténation sans bord axe Y" + }, + "compositingCoherenceMinDenoise": { + "paragraphs": [ + "Force de débruitage minimale pour le mode de cohérence", + "La force minimale de débruitage pour la région de cohérence lors de l'inpainting ou de l'outpainting" + ], + "heading": "Débruitage minimum" + }, + "refinerStart": { + "paragraphs": [ + "À quel moment du processus de génération le raffineur commencera-t-il à être utilisé.", + "0 signifie que le raffineur sera utilisé pour l'ensemble du processus de génération, 0,8 signifie que le raffineur sera utilisé pour les 20 % restants du processus de génération." + ], + "heading": "Démarrer le raffineur" + }, + "compositingMaskBlur": { + "heading": "Flou de masque", + "paragraphs": [ + "Le rayon de flou du masque." + ] + }, + "refinerSteps": { + "paragraphs": [ + "Nombre d'étapes qui seront effectuées pendant la partie de raffinage du processus de génération.", + "Similaire aux Étapes de Génération." + ], + "heading": "Étapes" + }, + "refinerCfgScale": { + "paragraphs": [ + "Contrôle dans quelle mesure le prompt influence le processus de génération.", + "Similaire à l'échelle de génération CFG." + ], + "heading": "Échelle CFG" + }, + "compositingCoherenceEdgeSize": { + "paragraphs": [ + "La taille de bord du passage de cohérence." + ], + "heading": "Taille de bord" + }, + "seamlessTilingXAxis": { + "heading": "Concaténation sans bord axe X", + "paragraphs": [ + "Concaténer une image de manière fluide le long de l'axe horizontal." + ] + }, + "creativity": { + "paragraphs": [ + "La créativité contrôle la quantité de liberté accordée au modèle lors de l'ajout de détails. Une faible créativité reste proche de l'image originale, tandis qu'une forte créativité permet plus de changements. Lors de l'utilisation d'un prompt, une forte créativité augmente l'influence du prompt." + ], + "heading": "Créativité" + }, + "structure": { + "heading": "Structure", + "paragraphs": [ + "La structure contrôle à quel point l'image de sortie respectera la mise en page de l'originale. Une faible structure permet des changements majeurs, tandis qu'une forte structure maintient strictement la composition et la mise en page d'origine." + ] + }, + "fluxDevLicense": { + "heading": "Licence non commerciale", + "paragraphs": [ + "Les modèles FLUX.1 [dev] sont sous licence non commerciale FLUX [dev]. Pour utiliser ce type de modèle à des fins commerciales dans Invoke, visitez notre site web pour en savoir plus." + ] + }, + "optimizedDenoising": { + "heading": "Image vers Image Optimisé", + "paragraphs": [ + "Activez « Image-vers-image optimisé » pour une échelle de force de débruitage plus progressive pour les transformations image-vers-image et d'inpainting avec les modèles Flux. Ce paramètre améliore la capacité à contrôler la quantité de changement appliquée à une image, mais peut être désactivé si vous préférez utiliser l'échelle de force de débruitage standard. Ce paramètre est encore en cours d'ajustement et est en bêta." + ] + }, + "upscaleModel": { + "paragraphs": [ + "Le modèle d'agrandissement redimensionne l'image à la taille de sortie avant que les détails ne soient ajoutés. Tout modèle d'agrandissement pris en charge peut être utilisé, mais certains sont spécialisés pour différents types d'images, comme les photos ou les dessins." + ], + "heading": "Modèle d'agrandissement" + }, + "ipAdapterMethod": { + "heading": "Méthode", + "paragraphs": [ + "Méthode pour appliquer l'adaptateur IP actuel." + ] + }, + "scale": { + "heading": "Échelle", + "paragraphs": [ + "L'échelle contrôle la taille de l'image de sortie et est basée sur un multiple de la résolution de l'image d'entrée. Par exemple, un agrandissement 2x sur une image de 1024x1024 produirait une sortie de 2048 x 2048." + ] + }, + "paramGuidance": { + "paragraphs": [ + "Contrôle de l'influence du prompt sur le processus de génération.", + "Des valeurs de guidage élevées peuvent entraîner une saturation excessive, et un guidage élevé ou faible peut entraîner des résultats de génération déformés. Le guidage ne s'applique qu'aux modèles FLUX DEV." + ], + "heading": "Guidage" + }, + "globalReferenceImage": { + "heading": "Image de Référence Globale", + "paragraphs": [ + "Applique une image de référence pour influencer l'ensemble de la génération." + ] + }, + "regionalReferenceImage": { + "heading": "Image de Référence Régionale", + "paragraphs": [ + "Pinceau pour appliquer une image de référence à des zones spécifiques." + ] + }, + "inpainting": { + "heading": "Inpainting", + "paragraphs": [ + "Contrôle la zone qui est modifiée, guidé par la force de débruitage." + ] + }, + "regionalGuidance": { + "heading": "Guide Régional", + "paragraphs": [ + "Pinceau pour guider l'emplacement des éléments provenant des prompts globaux." + ] + }, + "regionalGuidanceAndReferenceImage": { + "heading": "Guide régional et image de référence régionale", + "paragraphs": [ + "Pour le Guide Régional, utilisez le pinceau pour indiquer où les éléments des prompts globaux doivent apparaître.", + "Pour l'image de référence régionale, pinceau pour appliquer une image de référence à des zones spécifiques." + ] + }, + "rasterLayer": { + "heading": "Couche Rastérisation", + "paragraphs": [ + "Contenu basé sur les pixels de votre toile, utilisé lors de la génération d'images." + ] + } + }, + "dynamicPrompts": { + "seedBehaviour": { + "label": "Comportement de la graine", + "perPromptDesc": "Utiliser une graine différente pour chaque image", + "perIterationLabel": "Graine par Itération", + "perIterationDesc": "Utiliser une graine différente pour chaque itération", + "perPromptLabel": "Graine par Image" + }, + "maxPrompts": "Nombre maximum de Prompts", + "showDynamicPrompts": "Afficher les Prompts dynamiques", + "dynamicPrompts": "Prompts Dynamiques", + "promptsPreview": "Prévisualisation des Prompts", + "loading": "Génération des Pompts Dynamiques..." + }, + "metadata": { + "positivePrompt": "Prompt Positif", + "allPrompts": "Tous les Prompts", + "negativePrompt": "Prompt Négatif", + "metadata": "Métadonné", + "scheduler": "Planificateur", + "imageDetails": "Détails de l'Image", + "seed": "Graine", + "workflow": "Workflow", + "width": "Largeur", + "Threshold": "Seuil de bruit", + "noMetaData": "Aucune métadonnée trouvée", + "model": "Modèle", + "noImageDetails": "Aucun détail d'image trouvé", + "steps": "Étapes", + "cfgScale": "Échelle CFG", + "generationMode": "Mode Génération", + "height": "Hauteur", + "createdBy": "Créé par", + "strength": "Force d'image à image", + "vae": "VAE", + "noRecallParameters": "Aucun paramètres à rappeler trouvé", + "cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)", + "recallParameters": "Rappeler les paramètres", + "imageDimensions": "Dimensions de l'image", + "parameterSet": "Paramètre {{parameter}} défini", + "canvasV2Metadata": "Toile", + "guidance": "Guide", + "seamlessXAxis": "Axe X sans bords", + "seamlessYAxis": "Axe Y sans bords" + }, + "sdxl": { + "refinerStart": "Démarrer le Refiner", + "denoisingStrength": "Force de débruitage", + "steps": "Étapes", + "refinermodel": "Modèle de Refiner", + "scheduler": "Planificateur", + "cfgScale": "Échelle CFG", + "noModelsAvailable": "Aucun modèle disponible", + "posAestheticScore": "Score esthétique positif", + "loading": "Chargement...", + "negAestheticScore": "Score esthétique négatif", + "refiner": "Refiner", + "refinerSteps": "Étapes de raffinage" + }, + "nodes": { + "showMinimapnodes": "Afficher la MiniCarte", + "fitViewportNodes": "Ajuster la Vue", + "hideMinimapnodes": "Masquer MiniCarte", + "zoomOutNodes": "Dézoomer", + "zoomInNodes": "Zoomer", + "downloadWorkflow": "Exporter le Workflow au format JSON", + "loadWorkflow": "Charger un Workflow", + "reloadNodeTemplates": "Recharger les modèles de nœuds", + "animatedEdges": "Connexions animées", + "cannotConnectToSelf": "Impossible de se connecter à soi-même", + "edge": "Connexion", + "workflowAuthor": "Auteur", + "enum": "Énumération", + "integer": "Entier", + "inputMayOnlyHaveOneConnection": "L'entrée ne peut avoir qu'une seule connexion", + "noNodeSelected": "Aucun nœud sélectionné", + "nodeOpacity": "Opacité du nœud", + "workflowDescription": "Courte description", + "executionStateError": "Erreur", + "version": "Version", + "boolean": "Booléens", + "executionStateCompleted": "Terminé", + "colorCodeEdges": "Code de couleur des connexions", + "colorCodeEdgesHelp": "Code couleur des connexions en fonction de leurs champs connectés", + "currentImage": "Image actuelle", + "float": "Flottant", + "missingTemplate": "Nœud invalide : le nœud {{node}} de type {{type}} modèle manquant (non installé ?)", + "noWorkflow": "Pas de Workflow", + "validateConnectionsHelp": "Prévenir la création de connexions invalides et l'invocation de graphes invalides", + "workflowSettings": "Paramètres de l'Éditeur de Workflow", + "workflowValidation": "Erreur de validation du Workflow", + "executionStateInProgress": "En cours", + "node": "Noeud", + "scheduler": "Planificateur", + "notes": "Notes", + "notesDescription": "Ajouter des notes sur votre workflow", + "addNode": "Ajouter un nœud", + "problemSettingTitle": "Problème lors de définition du Titre", + "connectionWouldCreateCycle": "La connexion créerait un cycle", + "currentImageDescription": "Affiche l'image actuelle dans l'éditeur de nœuds", + "cannotConnectInputToInput": "Impossible de connecter l'entrée à l'entrée", + "addNodeToolTip": "Ajouter un nœud (Shift+A, Espace)", + "fullyContainNodesHelp": "Les nœuds doivent être entièrement à l'intérieur de la zone de sélection pour être sélectionnés", + "cannotConnectOutputToOutput": "Impossible de connecter la sortie à la sortie", + "loadingNodes": "Chargement des nœuds...", + "unknownField": "Champ inconnu", + "workflowNotes": "Notes", + "workflowTags": "Tags", + "animatedEdgesHelp": "Animer les connexions sélectionnées et les connexions associées aux nœuds sélectionnés", + "nodeTemplate": "Modèle de nœud", + "fieldTypesMustMatch": "Les types de champs doivent correspondre", + "fullyContainNodes": "Contient complètement les nœuds à sélectionner", + "nodeSearch": "Rechercher des nœuds", + "collection": "Collection", + "noOutputRecorded": "Aucun résultat enregistré", + "snapToGrid": "Aligner sur la grille", + "workflow": "Workflow", + "updateApp": "Mettre à jour l'application", + "updateNode": "Mettre à jour le nœud", + "nodeOutputs": "Sorties de nœud", + "noConnectionInProgress": "Aucune connexion en cours", + "nodeType": "Type de nœud", + "workflowContact": "Contact", + "unknownNode": "Nœud inconnu", + "workflowVersion": "Version", + "string": "Chaîne de caractères", + "workflowName": "Nom", + "snapToGridHelp": "Aligner les nœuds sur la grille lors du déplacement", + "unableToValidateWorkflow": "Impossible de valider le Workflow", + "validateConnections": "Valider les connexions et le graphique", + "unableToUpdateNodes_one": "Impossible de mettre à jour {{count}} nœud", + "unableToUpdateNodes_many": "Impossible de mettre à jour {{count}} nœuds", + "unableToUpdateNodes_other": "Impossible de mettre à jour {{count}} nœuds", + "cannotDuplicateConnection": "Impossible de créer des connexions en double", + "resetToDefaultValue": "Réinitialiser à la valeur par défaut", + "unknownNodeType": "Type de nœud inconnu", + "prototypeDesc": "Cette invocation est un prototype. Elle peut subir des modifications majeures lors des mises à jour de l'application et peut être supprimée à tout moment.", + "nodePack": "Paquet de nœuds", + "sourceNodeDoesNotExist": "Connexion invalide : le nœud source/de sortie {{node}} n'existe pas", + "sourceNodeFieldDoesNotExist": "Connexion invalide : {{node}}.{{field}} n'existe pas", + "unableToGetWorkflowVersion": "Impossible d'obtenir la version du schéma du Workflow", + "newWorkflowDesc2": "Votre workflow actuel comporte des modifications non enregistrées.", + "deletedInvalidEdge": "Connexion invalide supprimé {{source}} -> {{target}}", + "targetNodeDoesNotExist": "Connexion invalide : le nœud cible/entrée {{node}} n'existe pas", + "targetNodeFieldDoesNotExist": "Connexion invalide : le champ {{node}}.{{field}} n'existe pas", + "nodeVersion": "Version du noeud", + "clearWorkflowDesc2": "Votre workflow actuel comporte des modifications non enregistrées.", + "clearWorkflow": "Effacer le Workflow", + "clearWorkflowDesc": "Effacer ce workflow et en commencer un nouveau ?", + "unsupportedArrayItemType": "type d'élément de tableau non pris en charge \"{{type}}\"", + "collectionOrScalarFieldType": "{{name}} (Unique ou Collection)", + "unableToExtractEnumOptions": "impossible d'extraire les options d'énumération", + "unsupportedAnyOfLength": "trop de membres dans l'union ({{count}})", + "ipAdapter": "IP-Adapter", + "viewMode": "Utiliser en vue linéaire", + "collectionFieldType": "{{name}} (Collection)", + "newWorkflow": "Nouveau Workflow", + "outputFieldTypeParseError": "Impossible d'analyser le type du champ de sortie {{node}}.{{field}} ({{message}})", + "unsupportedMismatchedUnion": "type CollectionOrScalar non concordant avec les types de base {{firstType}} et {{secondType}}", + "unableToParseFieldType": "impossible d'analyser le type de champ", + "betaDesc": "Cette invocation est en version bêta. Tant qu'elle n'est pas stable, elle peut avoir des changements majeurs lors des mises à jour de l'application. Nous prévoyons de soutenir cette invocation à long terme.", + "unknownFieldType": "$t(nodes.unknownField) type : {{type}}", + "inputFieldTypeParseError": "Impossible d'analyser le type du champ d'entrée {{node}}.{{field}} ({{message}})", + "unableToExtractSchemaNameFromRef": "impossible d'extraire le nom du schéma à partir de la référence", + "editMode": "Modifier dans l'éditeur de Workflow", + "unknownErrorValidatingWorkflow": "Erreur inconnue lors de la validation du Workflow", + "updateAllNodes": "Mettre à jour les nœuds", + "allNodesUpdated": "Tous les nœuds mis à jour", + "newWorkflowDesc": "Créer un nouveau workflow ?", + "edit": "Modifier", + "noFieldsViewMode": "Ce workflow n'a aucun champ sélectionné à afficher. Consultez le workflow complet pour configurer les valeurs.", + "graph": "Graph", + "modelAccessError": "Impossible de trouver le modèle {{key}}, réinitialisation aux paramètres par défaut", + "showEdgeLabelsHelp": "Afficher le nom sur les connections, indiquant les nœuds connectés", + "showEdgeLabels": "Afficher le nom des connections", + "cannotMixAndMatchCollectionItemTypes": "Impossible de mélanger et d'associer des types d'éléments de collection", + "noGraph": "Pas de graphique", + "saveToGallery": "Enregistrer dans la galerie", + "missingFieldTemplate": "Modèle de champ manquant", + "missingNode": "Noeud d'invocation manquant", + "singleFieldType": "{{name}} (Unique)", + "missingInvocationTemplate": "Modèle d'invocation manquant", + "imageAccessError": "Impossible de trouver l'image {{image_name}}, réinitialisation à la valeur par défaut", + "boardAccessError": "Impossible de trouver la planche {{board_id}}, réinitialisation à la valeur par défaut", + "workflowHelpText": "Besoin d'aide ? Consultez notre guide sur Comment commencer avec les Workflows.", + "noWorkflows": "Aucun Workflows", + "noMatchingWorkflows": "Aucun Workflows correspondant", + "arithmeticSequence": "Séquence Arithmétique", + "uniformRandomDistribution": "Distribution Aléatoire Uniforme", + "noBatchGroup": "aucun groupe", + "generatorLoadFromFile": "Charger depuis un Fichier", + "dynamicPromptsRandom": "Prompts Dynamiques (Aléatoire)", + "linearDistribution": "Distribution Linéaire", + "generatorNRandomValues_one": "{{count}} valeur aléatoire", + "generatorNRandomValues_many": "{{count}} valeurs aléatoires", + "generatorNRandomValues_other": "{{count}} valeurs aléatoires", + "dynamicPromptsCombinatorial": "Prompts Dynamiques (Combinatoire)", + "parseString": "Analyser la chaine de charactères", + "internalDesc": "Cette invocation est utilisée internalement par Invoke. En fonction des mises à jours il est possible que des changements y soit effectués ou qu'elle soit supprimé sans prévention.", + "splitOn": "Diviser sur", + "generatorNoValues": "vide", + "addItem": "Ajouter un élément", + "specialDesc": "Cette invocation nécessite un traitement spécial dans l'application. Par exemple, les nœuds Batch sont utilisés pour mettre en file d'attente plusieurs graphes à partir d'un seul workflow.", + "unableToUpdateNode": "La mise à jour du nœud a échoué : nœud {{node}} de type {{type}} (peut nécessiter la suppression et la recréation).", + "deletedMissingNodeFieldFormElement": "Champ de formulaire manquant supprimé : nœud {{nodeId}} champ {{fieldName}}", + "nodeName": "Nom du nœud", + "description": "Description", + "loadWorkflowDesc": "Charger le workflow ?", + "missingSourceOrTargetNode": "Nœud source ou cible manquant", + "generatorImagesCategory": "Catégorie", + "generatorImagesFromBoard": "Images de la Planche", + "missingSourceOrTargetHandle": "Manque de gestionnaire source ou cible", + "loadWorkflowDesc2": "Votre workflow actuel contient des modifications non enregistrées.", + "generatorImages_one": "{{count}} image", + "generatorImages_many": "{{count}} images", + "generatorImages_other": "{{count}} images" + }, + "models": { + "noMatchingModels": "Aucun modèle correspondant", + "noModelsAvailable": "Aucun modèle disponible", + "loading": "chargement", + "selectModel": "Sélectionner un modèle", + "lora": "LoRA", + "noRefinerModelsInstalled": "Aucun modèle SDXL Refiner installé", + "addLora": "Ajouter LoRA", + "defaultVAE": "VAE par défaut", + "concepts": "Concepts" + }, + "workflows": { + "workflowLibrary": "Bibliothèque", + "loading": "Chargement des Workflows", + "workflowCleared": "Workflow effacé", + "deleteWorkflow": "Supprimer le Workflow", + "uploadWorkflow": "Charger à partir d'un fichier", + "workflowName": "Nom du Workflow", + "unnamedWorkflow": "Workflow sans nom", + "saveWorkflowAs": "Enregistrer le Workflow sous", + "workflows": "Workflows", + "savingWorkflow": "Enregistrement du Workflow...", + "saveWorkflowToProject": "Enregistrer le Workflow dans le projet", + "downloadWorkflow": "Enregistrer dans le fichier", + "saveWorkflow": "Enregistrer le Workflow", + "problemSavingWorkflow": "Problème de sauvegarde du Workflow", + "workflowEditorMenu": "Menu de l'Éditeur de Workflow", + "newWorkflowCreated": "Nouveau Workflow créé", + "workflowSaved": "Workflow enregistré", + "noWorkflows": "Pas de Workflows", + "ascending": "Ascendant", + "loadFromGraph": "Charger le Workflow à partir du graphique", + "descending": "Descendant", + "created": "Créé", + "updated": "Mis à jour", + "loadWorkflow": "$t(common.load) Workflow", + "convertGraph": "Convertir le graphique", + "opened": "Ouvert", + "name": "Nom", + "autoLayout": "Mise en page automatique", + "copyShareLink": "Copier le lien de partage", + "chooseWorkflowFromLibrary": "Choisir le Workflow dans la Bibliothèque", + "edit": "Modifer", + "deleteWorkflow2": "Êtes-vous sûr de vouloir supprimer ce Workflow ? Cette action ne peut pas être annulé.", + "download": "Télécharger", + "copyShareLinkForWorkflow": "Copier le lien de partage pour le Workflow", + "delete": "Supprimer", + "builder": { + "component": "Composant", + "numberInput": "Entrée de nombre", + "slider": "Curseur", + "both": "Les deux", + "singleLine": "Ligne unique", + "multiLine": "Multi Ligne", + "headingPlaceholder": "En-tête vide", + "emptyRootPlaceholderEditMode": "Faites glisser un élément de formulaire ou un champ de nœud ici pour commencer.", + "containerPlaceholder": "Conteneur Vide", + "row": "Ligne", + "column": "Colonne", + "layout": "Mise en page", + "nodeField": "Champ de nœud", + "zoomToNode": "Zoomer sur le nœud", + "nodeFieldTooltip": "Pour ajouter un champ de nœud, cliquez sur le petit bouton plus sur le champ dans l'Éditeur de Workflow, ou faites glisser le champ par son nom dans le formulaire.", + "addToForm": "Ajouter au formulaire", + "label": "Étiquette", + "textPlaceholder": "Texte vide", + "builder": "Constructeur de Formulaire", + "resetAllNodeFields": "Réinitialiser tous les champs de nœud", + "deleteAllElements": "Supprimer tous les éléments de formulaire", + "showDescription": "Afficher la description" + } + }, + "whatsNew": { + "whatsNewInInvoke": "Quoi de neuf dans Invoke", + "watchRecentReleaseVideos": "Regarder les vidéos des dernières versions", + "items": [ + "FLUX Guidage Régional (bêta) : Notre version bêta de FLUX Guidage Régional est en ligne pour le contrôle des prompt régionaux.", + "Autres améliorations : mise en file d'attente par lots plus rapide, meilleur redimensionnement, sélecteur de couleurs amélioré et nœuds de métadonnées." + ], + "readReleaseNotes": "Notes de version" + }, + "ui": { + "tabs": { + "queue": "File d'attente", + "canvas": "Toile", + "upscaling": "Agrandissement", + "gallery": "Galerie", + "upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)", + "workflows": "Workflows", + "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)", + "models": "Modèles", + "modelsTab": "$t(ui.tabs.models) $t(common.tab)" + } + }, + "controlLayers": { + "newLayerFromImage": "Nouvelle couche à partir de l'image", + "sendToCanvas": "Envoyer vers la Toile", + "globalReferenceImage": "Image de référence globale", + "newCanvasFromImage": "Nouvelle Toile à partir de l'image", + "deleteSelected": "Supprimer la sélection", + "unlocked": "Déverrouillé", + "filter": { + "mediapipe_face_detection": { + "description": "Détecte les visages dans la couche sélectionnée en utilisant le modèle de détection de visages MediaPipe.", + "label": "Détection de visage MediaPipe", + "min_confidence": "Confiance minimale", + "max_faces": "Max Visages" + }, + "lineart_edge_detection": { + "coarse": "Grossier", + "description": "Génère une carte des contours à partir de la couche sélectionnée en utilisant le modèle de détection de contours Lineart.", + "label": "Détection de contours Lineart" + }, + "mlsd_detection": { + "score_threshold": "Seuil de score", + "label": "Détection de segments", + "description": "Génère une carte de segments de ligne à partir de la couche sélectionnée en utilisant le modèle de détection de segments MLSD.", + "distance_threshold": "Seuil de distance" + }, + "normal_map": { + "label": "Carte normale", + "description": "Génère une carte normale à partir de la couche sélectionnée." + }, + "pidi_edge_detection": { + "quantize_edges": "Quantifier les contours", + "scribble": "Esquisse", + "description": "Génère une carte des contours à partir de la couche sélectionnée en utilisant le modèle de détection de contours PiDiNet.", + "label": "Détection de contours PiDiNet" + }, + "filter": "Filtre", + "filters": "Filtres", + "filterType": "Type de filtre", + "reset": "Réinitialiser", + "spandrel_filter": { + "label": "Modèle Image-vers-Image", + "model": "Modèle", + "autoScale": "Échelle automatique", + "description": "Exécute un modèle d'image vers image sur le calque sélectionné.", + "autoScaleDesc": "Le modèle sélectionné sera exécuté jusqu'à ce que l'échelle cible soit atteinte.", + "scale": "Échelle cible" + }, + "canny_edge_detection": { + "label": "Détection des contours de Canny", + "low_threshold": "Seuil Inférieur", + "high_threshold": "Seuil Supérieur", + "description": "Génère une carte des contours à partir de la couche sélectionnée en utilisant l'algorithme de détection de contours de Canny." + }, + "color_map": { + "label": "Carte de couleurs", + "description": "Créer une carte des couleurs à partir de la couche sélectionnée.", + "tile_size": "Taille de tuile" + }, + "content_shuffle": { + "label": "Mélanger le contenu", + "scale_factor": "Facteur d'échelle", + "description": "Mélange le contenu de la couche sélectionnée, similaire à un effet de 'liquéfaction'." + }, + "depth_anything_depth_estimation": { + "model_size": "Taille du modèle", + "model_size_small_v2": "Petit v2", + "label": "Depth Anything", + "model_size_large": "Grand", + "model_size_base": "Base", + "model_size_small": "Petit", + "description": "Génère une carte de profondeur à partir de la couche sélectionnée en utilisant un modèle Depth Anything." + }, + "dw_openpose_detection": { + "draw_hands": "Dessiner les mains", + "label": "Détection DW OpenPose", + "description": "Détecte les poses humaines dans la couche sélectionnée en utilisant le modèle DW Openpose.", + "draw_face": "Dessiner le visage", + "draw_body": "Dessiner le corps" + }, + "hed_edge_detection": { + "scribble": "Esquisse", + "description": "Génère une carte des contours à partir de la couche sélectionnée en utilisant le modèle de détection de contours HED.", + "label": "Détection de contours HED" + }, + "autoProcess": "Traiter automatiquement", + "lineart_anime_edge_detection": { + "label": "Détection de contours Lineart Anime", + "description": "Génère une carte des contours à partir de la couche sélectionnée en utilisant le modèle de détection de contours Lineart Anime." + }, + "process": "Traiter", + "apply": "Appliquer", + "cancel": "Annuler", + "advanced": "Avancé", + "processingLayerWith": "Calque de traitement avec le filtre {{type}}.", + "forMoreControl": "Pour plus de contrôle, cliquez sur Avancé ci-dessous.", + "adjust_image": { + "b": "B (LAB)", + "blue": "Bleu (RGBA)", + "alpha": "Alpha (RGBA)", + "magenta": "Magenta (CMJN)", + "yellow": "Jaune (CMJN)", + "cb": "Cb (YCbCr)", + "cr": "Cr (YCbCr)", + "cyan": "Cyan (CMJN)", + "label": "Ajuster l'image", + "description": "Ajuste le canal sélectionné d'une image.", + "channel": "Canal", + "value_setting": "Valeur", + "scale_values": "Valeurs d'échelle", + "red": "Rouge (RGBA)", + "green": "Vert (RGBA)", + "black": "Noir (CMJN)", + "hue": "Teinte (HSV)", + "saturation": "Saturation (HSV)", + "value": "Valeur (HSV)", + "luminosity": "Luminosité (LAB)", + "a": "A (LAB)", + "y": "Y (YCbCr)" + }, + "img_blur": { + "label": "Flou de l'image", + "blur_type": "Type de flou", + "box_type": "Boîte", + "description": "Floute la couche sélectionnée.", + "blur_radius": "Rayon", + "gaussian_type": "Gaussien" + }, + "img_noise": { + "label": "Image de bruit", + "description": "Ajoute du bruit à la couche sélectionnée.", + "gaussian_type": "Gaussien", + "size": "Taille du bruit", + "noise_amount": "Quantité", + "noise_type": "Type de bruit", + "salt_and_pepper_type": "Sel et Poivre", + "noise_color": "Bruit coloré" + } + }, + "canvasContextMenu": { + "saveToGalleryGroup": "Enregistrer dans la galerie", + "saveCanvasToGallery": "Enregistrer la Toile dans la galerie", + "newRasterLayer": "Nouveau couche de rastérisation", + "canvasGroup": "Toile", + "cropCanvasToBbox": "Rogner la toile à la bounding box", + "saveBboxToGallery": "Enregistrer la bounding box dans la galerie", + "bboxGroup": "Créer à partir de la bounding box", + "newRegionalReferenceImage": "Nouvelle image de référence régionale", + "newGlobalReferenceImage": "Nouvelle image de référence globale", + "newControlLayer": "Nouveau couche de contrôle", + "newInpaintMask": "Nouveau Masque Inpaint", + "newRegionalGuidance": "Nouveau Guide Régional", + "copyToClipboard": "Copier dans le presse-papiers", + "copyBboxToClipboard": "Copier Bbox dans le presse-papiers", + "copyCanvasToClipboard": "Copier la Toile dans le presse-papiers" + }, + "bookmark": "Marque-page pour Changement Rapide", + "saveLayerToAssets": "Enregistrer la couche dans les ressources", + "enableTransparencyEffect": "Activer l'effet de transparence", + "hidingType": "Masquer {{type}}", + "settings": { + "snapToGrid": { + "off": "Désactivé", + "on": "Activé", + "label": "Aligner sur la grille" + }, + "invertBrushSizeScrollDirection": "Inverser le défilement pour la taille du pinceau", + "pressureSensitivity": "Sensibilité à la pression", + "preserveMask": { + "label": "Préserver la zone masquée", + "alert": "Préserver la zone masquée" + }, + "isolatedPreview": "Aperçu Isolé", + "isolatedStagingPreview": "Aperçu de l'attente isolé", + "isolatedLayerPreview": "Aperçu de la couche isolée", + "isolatedLayerPreviewDesc": "Pour afficher uniquement cette couche lors de l'exécution d'opérations telles que le filtrage ou la transformation." + }, + "transparency": "Transparence", + "moveBackward": "Reculer", + "rectangle": "Rectangle", + "saveCanvasToGallery": "Enregistrer la Toile dans la galerie", + "saveBboxToGallery": "Enregistrer la Bounding Box dans la Galerie", + "mergeVisible": "Fusionner visible", + "recalculateRects": "Recalculer les rectangles", + "clipToBbox": "Couper les traits à la bounding box", + "disableAutoNegative": "Désactiver l'Auto Négatif", + "addNegativePrompt": "Ajouter $t(controlLayers.negativePrompt)", + "addRegionalGuidance": "Ajouter $t(controlLayers.regionalGuidance)", + "layer_one": "Couche", + "layer_many": "Couches", + "layer_other": "Couches", + "dynamicGrid": "Grille dynamique", + "logDebugInfo": "Journaliser les informations de débogage", + "locked": "Verrouillé", + "fill": { + "fillColor": "Couleur de remplissage", + "horizontal": "Horizontal", + "diagonal": "Diagonale", + "crosshatch": "Hachures", + "solid": "Solide", + "grid": "Grille", + "fillStyle": "Style de remplissage", + "vertical": "Vertical" + }, + "tool": { + "brush": "Pinceau", + "colorPicker": "Pipette", + "eraser": "Gomme", + "rectangle": "Rectangle", + "bbox": "Bounding Box", + "move": "Déplacer", + "view": "Vue" + }, + "transform": { + "fitToBbox": "Ajuster à la bounding box", + "reset": "Réinitialiser", + "apply": "Appliquer", + "cancel": "Annuler", + "transform": "Transformer", + "fitMode": "Mode Ajusté", + "fitModeContain": "Contenir", + "fitModeCover": "Couvrir", + "fitModeFill": "Remplir" + }, + "rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)", + "rasterLayer_withCount_many": "Rastériser les couches", + "rasterLayer_withCount_other": "Rastériser les couches", + "stagingArea": { + "discard": "Jeter", + "discardAll": "Tout jeter", + "showResultsOn": "Afficher les résultats", + "showResultsOff": "Masquer les résultats", + "accept": "Accepter", + "previous": "Précédent", + "next": "Suivant", + "saveToGallery": "Enregistrer dans la galerie" + }, + "mergeVisibleError": "Erreur lors de la fusion des calques visibles", + "mergeVisibleOk": "Couches visibles fusionnées", + "clearHistory": "Effacer l'historique", + "addLayer": "Ajouter une couche", + "clearCaches": "Vider les caches", + "duplicate": "Dupliquer", + "enableAutoNegative": "Activer l'Auto Négatif", + "showHUD": "Afficher HUD", + "disableTransparencyEffect": "Désactiver l'effet de transparence", + "HUD": { + "entityStatus": { + "isHidden": "{{title}} est caché", + "isDisabled": "{{title}} est désactivé", + "isLocked": "{{title}} est verrouillé", + "isTransforming": "{{title}} est en train de se transformer", + "isFiltering": "{{title}} est en train de filtrer", + "isEmpty": "{{title}} est vide" + }, + "bbox": "Bounding Box", + "scaledBbox": "Bounding Box redimensionné" + }, + "opacity": "Opacité", + "savedToGalleryError": "Erreur lors de l'enregistrement dans la galerie", + "addInpaintMask": "Ajouter $t(controlLayers.inpaintMask)", + "canvas": "Toile", + "savedToGalleryOk": "Enregistré dans la galerie", + "addPositivePrompt": "Ajouter $t(controlLayers.prompt)", + "showProgressOnCanvas": "Afficher la progression sur la Toile", + "showingType": "Afficher {{type}}", + "addControlLayer": "Ajouter $t(controlLayers.controlLayer)", + "global": "Global", + "newGlobalReferenceImageOk": "Image de référence globale créée", + "regional": "Régional", + "newRegionalReferenceImageError": "Problème de création d'image de référence régionale", + "newControlLayerError": "Problème de création de la couche de contrôle", + "newRasterLayerOk": "Couche de Rastérisation créée", + "newControlLayerOk": "Couche de contrôle créée", + "newGlobalReferenceImageError": "Problème de création d'image de référence globale", + "newRegionalReferenceImageOk": "Image de référence régionale créée", + "newRasterLayerError": "Problème de création de couche de rastérisation", + "negativePrompt": "Prompt négatif", + "weight": "Poids", + "controlMode": { + "controlMode": "Mode de contrôle", + "balanced": "Équilibré", + "prompt": "Prompt", + "control": "Contrôle", + "megaControl": "Méga Contrôle" + }, + "replaceLayer": "Remplacer la couche", + "pullBboxIntoLayer": "Tirer la bounding box dans la couche", + "pullBboxIntoReferenceImage": "Insérer la Bounding Box dans l'image de référence", + "prompt": "Prompt", + "beginEndStepPercentShort": "Début/Fin %", + "ipAdapterMethod": { + "ipAdapterMethod": "Méthode d'IP Adapter", + "full": "Complet", + "style": "Style uniquement", + "composition": "Composition uniquement", + "fullDesc": "Applique le style visuel (couleurs, textures) et la composition (mise en page, structure).", + "styleDesc": "Applique un style visuel (couleurs, textures) sans tenir compte de sa mise en page.", + "compositionDesc": "Réplique la mise en page et la structure tout en ignorant le style de la référence." + }, + "fitBboxToLayers": "Ajuster la bounding box aux calques", + "regionIsEmpty": "La zone sélectionnée est vide", + "cropLayerToBbox": "Rogner la couche selon la bounding box", + "copyToClipboard": "Copier dans le presse-papiers", + "regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)", + "regionalGuidance_withCount_many": "Guidage Régional", + "regionalGuidance_withCount_other": "Guidage Régional", + "inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)", + "inpaintMask_withCount_many": "Remplir les masques", + "inpaintMask_withCount_other": "Remplir les masques", + "bboxOverlay": "Afficher la superposition des Bounding Box", + "moveToFront": "Déplacer vers le permier plan", + "moveToBack": "Déplacer vers l'arrière plan", + "moveForward": "Avancer", + "width": "Largeur", + "outputOnlyMaskedRegions": "Retourner uniquement les régions masquées", + "autoNegative": "Négatif automatique", + "maskFill": "Remplissage de masque", + "addRasterLayer": "Ajouter $t(controlLayers.rasterLayer)", + "rasterLayer": "Rastériser la Couche", + "controlLayer": "Control Layer", + "inpaintMask": "Masque de remplissage", + "deleteReferenceImage": "Supprimer l'image de référence", + "addReferenceImage": "Ajouter $t(controlLayers.referenceImage)", + "removeBookmark": "Supprimer le marque-page", + "regionalGuidance": "Guide régional", + "regionalReferenceImage": "Image de référence régionale", + "pullBboxIntoLayerOk": "Bounding Box insérée dans la couche", + "pullBboxIntoReferenceImageError": "Problème de l'insertion de la Bounding Box dans l'image de référence", + "referenceImage": "Image de référence", + "pullBboxIntoLayerError": "Problème d'insertion de la bounding box dans la couche", + "pullBboxIntoReferenceImageOk": "Bounding Box insérée dans l'Image de référence", + "controlLayer_withCount_one": "$t(controlLayers.controlLayer)", + "controlLayer_withCount_many": "Controler les couches", + "controlLayer_withCount_other": "Controler les couches", + "copyInpaintMaskTo": "Copier $t(controlLayers.inpaintMask) vers", + "copyRegionalGuidanceTo": "Copier $t(controlLayers.regionalGuidance) vers", + "convertRasterLayerTo": "Convertir $t(controlLayers.rasterLayer) vers", + "selectObject": { + "selectObject": "Sélectionner l'objet", + "clickToAdd": "Cliquez sur la couche pour ajouter un point", + "apply": "Appliquer", + "cancel": "Annuler", + "dragToMove": "Faites glisser un point pour le déplacer", + "clickToRemove": "Cliquez sur un point pour le supprimer", + "include": "Inclure", + "invertSelection": "Sélection Inversée", + "saveAs": "Enregistrer sous", + "neutral": "Neutre", + "pointType": "Type de point", + "exclude": "Exclure", + "process": "Traiter", + "reset": "Réinitialiser" + }, + "convertRegionalGuidanceTo": "Convertir $t(controlLayers.regionalGuidance) vers", + "copyRasterLayerTo": "Copier $t(controlLayers.rasterLayer) vers", + "newControlLayer": "Nouveau $t(controlLayers.controlLayer)", + "newRegionalGuidance": "Nouveau $t(controlLayers.regionalGuidance)", + "convertControlLayerTo": "Convertir $t(controlLayers.controlLayer) vers", + "convertInpaintMaskTo": "Convertir $t(controlLayers.inpaintMask) vers", + "copyControlLayerTo": "Copier $t(controlLayers.controlLayer) vers", + "newInpaintMask": "Nouveau $t(controlLayers.inpaintMask)", + "newRasterLayer": "Nouveau $t(controlLayers.rasterLayer)", + "mergingLayers": "Fusionner les couches", + "resetCanvasLayers": "Réinitialiser les couches de la toile", + "resetGenerationSettings": "Réinitialiser les paramètres de génération", + "mergeDown": "Fusionner", + "controlLayerEmptyState": "Télécharger une image, faites glisser une image depuis la galerie sur ce calque, ou dessinez sur la toile pour commencer.", + "asRasterLayer": "En tant que $t(controlLayers.rasterLayer)", + "asRasterLayerResize": "En tant que $t(controlLayers.rasterLayer) (Redimensionner)", + "asControlLayer": "En tant que $t(controlLayers.controlLayer)", + "asControlLayerResize": "En $t(controlLayers.controlLayer) (Redimensionner)", + "newSession": "Nouvelle session", + "warnings": { + "controlAdapterIncompatibleBaseModel": "modèle de base de la couche de contrôle incompatible", + "controlAdapterNoControl": "aucun contrôle sélectionné/dessiné", + "rgNoPromptsOrIPAdapters": "pas de textes d'instructions ni d'images de référence", + "rgAutoNegativeNotSupported": "Auto-négatif non pris en charge pour le modèle de base sélectionné", + "rgNoRegion": "aucune région dessinée", + "ipAdapterNoModelSelected": "aucun modèle d'image de référence sélectionné", + "rgReferenceImagesNotSupported": "Les images de référence régionales ne sont pas prises en charge pour le modèle de base sélectionné", + "problemsFound": "Problèmes trouvés", + "unsupportedModel": "couche non prise en charge pour le modèle de base sélectionné", + "rgNegativePromptNotSupported": "Prompt négatif non pris en charge pour le modèle de base sélectionné", + "ipAdapterIncompatibleBaseModel": "modèle de base d'image de référence incompatible", + "controlAdapterNoModelSelected": "aucun modèle de couche de contrôle sélectionné", + "ipAdapterNoImageSelected": "Aucune image de référence sélectionnée." + }, + "pasteTo": "Coller vers", + "pasteToAssets": "Ressources", + "pasteToAssetsDesc": "Coller dans les ressources", + "pasteToBbox": "Bbox", + "regionCopiedToClipboard": "{{region}} Copié dans le presse-papiers", + "copyRegionError": "Erreur de copie {{region}}", + "pasteToCanvas": "Toile", + "errors": { + "unableToFindImage": "Impossible de trouver l'image", + "unableToLoadImage": "Impossible de charger l'image" + }, + "referenceImageRegional": "Image de référence (régionale)", + "pasteToBboxDesc": "Nouvelle couche (dans Bbox)", + "pasteToCanvasDesc": "Nouvelle couche (dans la Toile)", + "useImage": "Utiliser l'image", + "referenceImageEmptyState": "Séléctionner une image ou faites glisser une image depuis la galerie sur cette couche pour commencer." + }, + "upscaling": { + "exceedsMaxSizeDetails": "La limite maximale d'agrandissement est de {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixels. Veuillez essayer une image plus petite ou réduire votre sélection d'échelle.", + "upscale": "Agrandissement", + "exceedsMaxSize": "Les paramètres d'agrandissement dépassent la limite de taille maximale", + "structure": "Structure", + "creativity": "Créativité", + "upscaleModel": "Modèle d'Agrandissement", + "tileControlNetModelDesc": "Modèle ControlNet pour l'architecture principale choisie", + "upscaleModelDesc": "Modèle d'agrandissement (image vers image)", + "missingModelsWarning": "Visitez le Gestionnaire de Modèles pour installer les modèles requis :", + "postProcessingMissingModelWarning": "Visitez le Gestionnaire de Modèles pour installer un modèle de post-traitement (image vers image).", + "scale": "Échelle", + "mainModelDesc": "Modèle principal (architecture SD1.5 ou SDXL)", + "postProcessingModel": "Modèle de post-traitement", + "missingUpscaleModel": "Modèle d'agrandissement manquant", + "missingUpscaleInitialImage": "Image initiale manquante pour l'agrandissement", + "missingTileControlNetModel": "Aucun modèle ControlNet valide installé", + "incompatibleBaseModelDesc": "L'upscaling est pris en charge uniquement pour les modèles d'architecture SD1.5 et SDXL. Changez le modèle principal pour activer l'upscaling.", + "incompatibleBaseModel": "Modèle principal non pris en charge pour l'upscaling" + }, + "stylePresets": { + "deleteTemplate": "Supprimer le template", + "editTemplate": "Modifier le template", + "exportFailed": "Impossible de générer et de télécharger le CSV", + "name": "Nom", + "acceptedColumnsKeys": "Colonnes/clés acceptées :", + "promptTemplatesDesc1": "Les templates de prompt ajoutent du texte aux prompts que vous écrivez dans la zone de saisie.", + "private": "Privé", + "searchByName": "Rechercher par nom", + "viewList": "Afficher la liste des templates", + "noTemplates": "Aucun templates", + "insertPlaceholder": "Insérer un placeholder", + "defaultTemplates": "Template pré-défini", + "deleteImage": "Supprimer l'image", + "createPromptTemplate": "Créer un template de prompt", + "negativePrompt": "Prompt négatif", + "promptTemplatesDesc3": "Si vous omettez le placeholder, le template sera ajouté à la fin de votre prompt.", + "positivePrompt": "Prompt positif", + "choosePromptTemplate": "Choisir un template de prompt", + "toggleViewMode": "Basculer le mode d'affichage", + "updatePromptTemplate": "Mettre à jour le template de prompt", + "flatten": "Intégrer le template sélectionné dans le prompt actuel", + "myTemplates": "Mes Templates", + "type": "Type", + "exportDownloaded": "Exportation téléchargée", + "clearTemplateSelection": "Supprimer la sélection de template", + "promptTemplateCleared": "Template de prompt effacé", + "templateDeleted": "Template de prompt supprimé", + "exportPromptTemplates": "Exporter mes templates de prompt (CSV)", + "nameColumn": "'nom'", + "positivePromptColumn": "\"prompt\" ou \"prompt_positif\"", + "useForTemplate": "Utiliser pour le template de prompt", + "uploadImage": "Importer une image", + "importTemplates": "Importer des templates de prompt (CSV/JSON)", + "negativePromptColumn": "'prompt_négatif'", + "deleteTemplate2": "Êtes-vous sûr de vouloir supprimer ce template ? Cette action ne peut pas être annulée.", + "preview": "Aperçu", + "shared": "Partagé", + "noMatchingTemplates": "Aucun templates correspondant", + "sharedTemplates": "Template partagés", + "unableToDeleteTemplate": "Impossible de supprimer le template de prompt", + "active": "Actif", + "copyTemplate": "Copier le template", + "viewModeTooltip": "Voici à quoi ressemblera votre prompt avec le template actuellement sélectionné. Pour modifier votre prompt, cliquez n'importe où dans la zone de texte.", + "promptTemplatesDesc2": "Utilisez la chaîne de remplacement
{{placeholder}}
pour spécifier où votre prompt doit être inclus dans le template." + }, + "system": { + "logNamespaces": { + "config": "Configuration", + "canvas": "Toile", + "generation": "Génération", + "workflows": "Workflows", + "system": "Système", + "models": "Modèles", + "logNamespaces": "Journalisation des espaces de noms", + "queue": "File d'attente", + "events": "Événements", + "metadata": "Métadonnées", + "gallery": "Galerie", + "dnd": "Glisser et déposer" + }, + "logLevel": { + "trace": "Trace", + "logLevel": "Niveau de journalisation", + "debug": "Debug", + "error": "Erreur", + "info": "Info", + "warn": "Alerte", + "fatal": "Fatal" + }, + "enableLogging": "Activer la journalisation" + }, + "newUserExperience": { + "toGetStarted": "Pour commencer, saisissez un prompt dans la boîte et cliquez sur Invoke pour générer votre première image. Sélectionnez un template de prompt pour améliorer les résultats. Vous pouvez choisir de sauvegarder vos images directement dans la Galerie ou de les modifier sur la Toile.", + "gettingStartedSeries": "Vous souhaitez plus de conseils ? Consultez notre Série de démarrage pour des astuces sur l'exploitation du plein potentiel de l'Invoke Studio.", + "noModelsInstalled": "Il semble qu'aucun modèle ne soit installé", + "toGetStartedLocal": "Pour commencer, assurez-vous de télécharger ou d'importer des modèles nécessaires pour exécuter Invoke. Ensuite, saisissez le prompt dans la boîte et cliquez sur Invoke pour générer votre première image. Sélectionnez un template de prompt pour améliorer les résultats. Vous pouvez choisir de sauvegarder vos images directement sur Galerie ou les modifier sur la Toile.", + "lowVRAMMode": "Pour de meilleures performances, suivez notre guide Low VRAM." + }, + "supportVideos": { + "watch": "Regarder", + "gettingStarted": "Commencer", + "supportVideos": "Vidéos d'assistance" + }, + "modelCache": { + "clear": "Effacer le cache du modèle", + "clearSucceeded": "Cache du modèle effacée", + "clearFailed": "Problème de nettoyage du cache du modèle" + } +} diff --git a/invokeai/frontend/web/public/locales/he.json b/invokeai/frontend/web/public/locales/he.json new file mode 100644 index 00000000000..01c2e9e51bf --- /dev/null +++ b/invokeai/frontend/web/public/locales/he.json @@ -0,0 +1,98 @@ +{ + "modelManager": { + "height": "גובה", + "load": "טען", + "search": "חיפוש", + "allModels": "כל המודלים", + "modelUpdated": "מודל עודכן", + "manual": "ידני", + "name": "שם", + "description": "תיאור", + "config": "תצורה", + "repo_id": "מזהה מאגר", + "width": "רוחב", + "addModel": "הוסף מודל", + "active": "פעיל", + "selected": "נבחר", + "deleteModel": "מחיקת מודל", + "deleteConfig": "מחיקת תצורה", + "convertToDiffusersHelpText5": "אנא ודא/י שיש לך מספיק מקום בדיסק. גדלי מודלים בדרך כלל הינם בין 4GB-7GB.", + "convertToDiffusersHelpText1": "מודל זה יומר לפורמט 🧨 המפזרים.", + "convertToDiffusersHelpText2": "תהליך זה יחליף את הרשומה של מנהל המודלים שלך בגרסת המפזרים של אותו המודל.", + "convertToDiffusersHelpText6": "האם ברצונך להמיר מודל זה?", + "modelConverted": "מודל הומר", + "alpha": "אלפא", + "modelManager": "מנהל המודלים", + "model": "מודל", + "availableModels": "מודלים זמינים", + "delete": "מחיקה", + "deleteMsg1": "האם אתה בטוח שברצונך למחוק רשומת מודל זו מ- InvokeAI?", + "deleteMsg2": "פעולה זו לא תמחק את קובץ נקודת הביקורת מהדיסק שלך. ניתן לקרוא אותם מחדש במידת הצורך.", + "convertToDiffusers": "המרה למפזרים", + "convert": "המרה", + "convertToDiffusersHelpText3": "קובץ נקודת הביקורת שלך בדיסק לא יימחק או ישונה בכל מקרה. אתה יכול להוסיף את נקודת הביקורת שלך למנהל המודלים שוב אם תרצה בכך.", + "convertToDiffusersHelpText4": "זהו תהליך חד פעמי בלבד. התהליך עשוי לקחת בסביבות 30-60 שניות, תלוי במפרט המחשב שלך." + }, + "common": { + "languagePickerLabel": "בחירת שפה", + "githubLabel": "גיטהאב", + "discordLabel": "דיסקורד", + "settingsLabel": "הגדרות", + "img2img": "תמונה לתמונה", + "nodes": "צמתים", + "statusDisconnected": "מנותק", + "hotkeysLabel": "מקשים חמים", + "reportBugLabel": "דווח באג", + "upload": "העלאה", + "load": "טעינה", + "back": "אחורה" + }, + "gallery": { + "galleryImageSize": "גודל תמונה", + "gallerySettings": "הגדרות גלריה", + "autoSwitchNewImages": "החלף אוטומטית לתמונות חדשות" + }, + "parameters": { + "images": "תמונות", + "steps": "צעדים", + "cfgScale": "סולם CFG", + "width": "רוחב", + "height": "גובה", + "seed": "זרע", + "type": "סוג", + "strength": "חוזק", + "denoisingStrength": "חוזק מנטרל הרעש", + "scaleBeforeProcessing": "שנה קנה מידה לפני עיבוד", + "scaledWidth": "קנה מידה לאחר שינוי W", + "scaledHeight": "קנה מידה לאחר שינוי H", + "infillMethod": "שיטת מילוי", + "tileSize": "גודל אריח", + "symmetry": "סימטריה", + "copyImage": "העתקת תמונה", + "usePrompt": "שימוש בבקשה", + "useSeed": "שימוש בזרע", + "useAll": "שימוש בהכל", + "info": "פרטים", + "shuffle": "ערבוב", + "noiseThreshold": "סף רעש", + "perlinNoise": "רעש פרלין", + "imageFit": "התאמת תמונה ראשונית לגודל הפלט", + "general": "כללי", + "upscaling": "מגדיל את קנה מידה", + "scale": "סולם" + }, + "settings": { + "models": "מודלים", + "displayInProgress": "הצגת תמונות בתהליך", + "confirmOnDelete": "אישור בעת המחיקה", + "resetWebUI": "איפוס ממשק משתמש", + "resetWebUIDesc1": "איפוס ממשק המשתמש האינטרנטי מאפס רק את המטמון המקומי של הדפדפן של התמונות וההגדרות שנשמרו. זה לא מוחק תמונות מהדיסק.", + "resetComplete": "ממשק המשתמש אופס. יש לבצע רענון דף בכדי לטעון אותו מחדש.", + "resetWebUIDesc2": "אם תמונות לא מופיעות בגלריה או שמשהו אחר לא עובד, נא לנסות איפוס /או אתחול לפני שליחת תקלה ב-GitHub." + }, + "toast": { + "uploadFailed": "העלאה נכשלה", + "imageCopied": "התמונה הועתקה", + "parametersNotSet": "פרמטרים לא הוגדרו" + } +} diff --git a/invokeai/frontend/web/public/locales/hu.json b/invokeai/frontend/web/public/locales/hu.json new file mode 100644 index 00000000000..2624fa03fd8 --- /dev/null +++ b/invokeai/frontend/web/public/locales/hu.json @@ -0,0 +1,34 @@ +{ + "accessibility": { + "mode": "Mód", + "uploadImage": "Fénykép feltöltése", + "nextImage": "Következő kép", + "previousImage": "Előző kép", + "menu": "Menü" + }, + "boards": { + "cancel": "Mégsem", + "loading": "Betöltés..." + }, + "accordions": { + "image": { + "title": "Kép" + } + }, + "common": { + "accept": "Elfogad", + "ai": "ai", + "back": "Vissza", + "cancel": "Mégsem", + "or": "vagy", + "details": "Részletek", + "error": "Hiba", + "file": "Fájl", + "githubLabel": "Github", + "hotkeysLabel": "Gyorsbillentyűk", + "delete": "Törlés", + "data": "Adat", + "discordLabel": "Discord", + "folder": "Mappa" + } +} diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json new file mode 100644 index 00000000000..382bd8e5238 --- /dev/null +++ b/invokeai/frontend/web/public/locales/it.json @@ -0,0 +1,3500 @@ +{ + "common": { + "hotkeysLabel": "Tasti di scelta rapida", + "languagePickerLabel": "Lingua", + "reportBugLabel": "Segnala un errore", + "settingsLabel": "Impostazioni", + "img2img": "Immagine a Immagine", + "nodes": "Flussi di lavoro", + "upload": "Caricamento", + "load": "Carica", + "back": "Indietro", + "statusDisconnected": "Disconnesso", + "githubLabel": "GitHub", + "discordLabel": "Discord", + "loading": "Caricamento in corso", + "postprocessing": "Post Elaborazione", + "txt2img": "Testo a Immagine", + "accept": "Accetta", + "cancel": "Annulla", + "linear": "Lineare", + "random": "Casuale", + "openInNewTab": "Apri in una nuova scheda", + "areYouSure": "Sei sicuro?", + "dontAskMeAgain": "Non chiedermelo più", + "batch": "Gestione Lotto", + "modelManager": "Gestione Modelli", + "communityLabel": "Comunità", + "advanced": "Avanzate", + "learnMore": "Per saperne di più", + "ipAdapter": "Adattatore IP", + "t2iAdapter": "Adattatore T2I", + "controlNet": "ControlNet", + "auto": "Automatico", + "simple": "Semplice", + "details": "Dettagli", + "format": "Formato", + "unknown": "Sconosciuto", + "folder": "Cartella", + "error": "Errore", + "installed": "Installato", + "template": "Schema", + "outputs": "Risultati", + "data": "Dati", + "somethingWentWrong": "Qualcosa è andato storto", + "copyError": "Errore $t(gallery.copy)", + "input": "Ingresso", + "unknownError": "Errore sconosciuto", + "updated": "Aggiornato", + "save": "Salva", + "created": "Creato", + "delete": "Elimina", + "orderBy": "Ordina per", + "saveAs": "Salva come", + "direction": "Direzione", + "or": "o", + "red": "Rosso", + "aboutHeading": "Possiedi il tuo potere creativo", + "aboutDesc": "Utilizzi Invoke per lavoro? Guarda qui:", + "green": "Verde", + "blue": "Blu", + "alpha": "Alfa", + "copy": "Copia", + "on": "Acceso", + "checkpoint": "Checkpoint", + "safetensors": "Safetensors", + "ai": "ia", + "file": "File", + "toResolve": "Da risolvere", + "add": "Aggiungi", + "beta": "Beta", + "positivePrompt": "Prompt positivo", + "negativePrompt": "Prompt negativo", + "selected": "Selezionato", + "editor": "Editor", + "tab": "Scheda", + "enabled": "Abilitato", + "disabled": "Disabilitato", + "dontShowMeThese": "Non mostrare più", + "openInViewer": "Apri nel visualizzatore", + "apply": "Applica", + "loadingImage": "Caricamento immagine", + "off": "Spento", + "edit": "Modifica", + "placeholderSelectAModel": "Seleziona un modello", + "reset": "Reimposta", + "none": "Niente", + "new": "Nuovo", + "view": "Vista", + "close": "Chiudi", + "clipboard": "Appunti", + "ok": "Ok", + "generating": "Generazione", + "loadingModel": "Caricamento del modello", + "warnings": "Avvisi", + "step": "Passo", + "values": "Valori", + "start": "Inizio", + "end": "Fine", + "seed": "Seme", + "combinatorial": "Combinatorio", + "count": "Quantità", + "board": "Bacheca", + "layout": "Schema", + "row": "Riga", + "column": "Colonna", + "saveChanges": "Salva modifiche", + "error_withCount_one": "{{count}} errore", + "error_withCount_many": "{{count}} errori", + "error_withCount_other": "{{count}} errori", + "value": "Valore", + "label": "Etichetta", + "systemInformation": "Informazioni di sistema", + "noMatches": "Nessuna corrispondenza", + "noOptions": "Nessuna opzione", + "model_withCount_one": "{{count}} modello", + "model_withCount_many": "{{count}} modelli", + "model_withCount_other": "{{count}} modelli", + "options_withCount_one": "{{count}} opzione", + "options_withCount_many": "{{count}} opzioni", + "options_withCount_other": "{{count}} opzioni", + "search": "Cerca", + "clear": "Cancella", + "compactView": "Vista compatta", + "fullView": "Vista completa", + "removeNegativePrompt": "Rimuovi prompt negativo", + "addNegativePrompt": "Aggiungi prompt negativo", + "selectYourModel": "Seleziona il modello", + "goTo": "Vai a", + "imageFailedToLoad": "Impossibile caricare l'immagine", + "localSystem": "Sistema locale", + "notInstalled": "Non $t(common.installed)", + "prevPage": "Pagina precedente", + "nextPage": "Pagina successiva", + "resetToDefaults": "Ripristina impostazioni predefinite", + "crop": "Ritaglia", + "editName": "Modifica nome", + "fitView": "Adatta la vista", + "minimize": "Minimizza", + "next": "Prossimo", + "noMatchingItems": "Nessun articolo corrispondente", + "notifications": "Notifiche", + "previous": "Precedente", + "removeFromCollection": "Rimuovi dalla raccolta", + "resetView": "Ripristina la vista", + "saveToAssets": "Salva nelle risorse", + "settings": "Impostazioni", + "toggleRgbHex": "Attiva/disattiva RGB/HEX", + "unpin": "Sblocca", + "openSlider": "Apri il cursore", + "collapseAll": "Comprimi tutto", + "expandAll": "Espandi tutto" + }, + "gallery": { + "galleryImageSize": "Dimensione dell'immagine", + "gallerySettings": "Impostazioni della galleria", + "autoSwitchNewImages": "Passaggio automatico a nuove immagini", + "deleteImage_one": "Elimina l'immagine", + "deleteImage_many": "Elimina {{count}} immagini", + "deleteImage_other": "Elimina {{count}} immagini", + "deleteImagePermanent": "Le immagini eliminate non possono essere ripristinate.", + "autoAssignBoardOnClick": "Assegna automaticamente la bacheca al clic", + "featuresWillReset": "Se elimini questa immagine, quelle funzionalità verranno immediatamente ripristinate.", + "loading": "Caricamento in corso", + "currentlyInUse": "Questa immagine è attualmente utilizzata nelle seguenti funzionalità:", + "copy": "Copia", + "download": "Scarica", + "downloadSelection": "Scarica gli elementi selezionati", + "noImageSelected": "Nessuna immagine selezionata", + "deleteSelection": "Elimina la selezione", + "image": "immagine", + "drop": "Rilascia", + "unstarImage": "Rimuovi contrassegno", + "dropOrUpload": "Rilascia o carica", + "starImage": "Contrassegna", + "dropToUpload": "$t(gallery.drop) per aggiornare", + "bulkDownloadRequested": "Preparazione del download", + "bulkDownloadRequestedDesc": "La tua richiesta di download è in preparazione. L'operazione potrebbe richiedere alcuni istanti.", + "bulkDownloadRequestFailed": "Problema durante la preparazione del download", + "bulkDownloadFailed": "Scaricamento fallito", + "alwaysShowImageSizeBadge": "Mostra sempre le dimensioni dell'immagine", + "openInViewer": "Apri nel visualizzatore", + "selectForCompare": "Seleziona per il confronto", + "slider": "Cursore", + "sideBySide": "Fianco a Fianco", + "compareImage": "Immagine di confronto", + "viewerImage": "Immagine visualizzata", + "hover": "Al passaggio del mouse", + "swapImages": "Scambia le immagini", + "stretchToFit": "Scala per adattare", + "exitCompare": "Esci dal confronto", + "compareHelp1": "Tieni premuto Alt mentre fai clic su un'immagine della galleria o usi i tasti freccia per cambiare l'immagine di confronto.", + "compareHelp2": "Premi M per scorrere le modalità di confronto.", + "compareHelp3": "Premi C per scambiare le immagini confrontate.", + "compareHelp4": "Premi Z o Esc per uscire.", + "newestFirst": "Prima i più nuovi", + "oldestFirst": "Prima i più vecchi", + "sortDirection": "Direzione dell'ordinamento", + "showStarredImagesFirst": "Mostra prima le immagini contrassegnate", + "showArchivedBoards": "Mostra le bacheche archiviate", + "searchImages": "Ricerca per metadati", + "displayBoardSearch": "Ricerca nella Bacheca", + "displaySearch": "Ricerca immagine", + "selectAllOnPage": "Seleziona tutto nella pagina", + "exitBoardSearch": "Esci da Ricerca bacheca", + "exitSearch": "Esci dalla ricerca immagini", + "go": "Vai", + "move": "Sposta", + "gallery": "Galleria", + "imagesTab": "Immagini create e salvate in Invoke.", + "assetsTab": "File che hai caricato per usarli nei tuoi progetti.", + "boardsSettings": "Impostazioni Bacheche", + "imagesSettings": "Impostazioni Immagini Galleria", + "assets": "Risorse", + "images": "Immagini", + "useForPromptGeneration": "Usa per generare il prompt", + "jump": "Salta", + "noImagesInGallery": "Nessuna immagine da visualizzare", + "unableToLoad": "Impossibile caricare la Galleria", + "selectAnImageToCompare": "Seleziona un'immagine da confrontare", + "openViewer": "Apri Visualizzatore", + "closeViewer": "Chiudi Visualizzatore", + "usePagedGalleryView": "Utilizza la visualizzazione Galleria a pagine", + "loadingGallery": "Caricamento galleria in corso...", + "loadingMetadata": "Caricamento dei metadati in corso...", + "noImagesFound": "Nessuna immagine trovata", + "bulkDownloadReady": "Download pronto", + "clickToDownload": "Clicca qui per scaricare" + }, + "hotkeys": { + "searchHotkeys": "Cerca tasti di scelta rapida", + "noHotkeysFound": "Nessun tasto di scelta rapida trovato", + "clearSearch": "Cancella ricerca", + "app": { + "selectCanvasTab": { + "title": "Seleziona la scheda Tela", + "desc": "Seleziona la scheda Tela." + }, + "title": "Applicazione", + "invoke": { + "desc": "Metti in coda una generazione, aggiungendola alla fine della coda." + }, + "invokeFront": { + "title": "Invoke (Fronte)", + "desc": "Metti in coda una generazione, aggiungendola all'inizio della coda." + }, + "cancelQueueItem": { + "desc": "Annulla l'elemento della coda in elaborazione.", + "title": "Annulla" + }, + "clearQueue": { + "title": "Cancella la coda", + "desc": "Annulla e cancella tutti gli elementi in coda." + }, + "selectUpscalingTab": { + "title": "Seleziona la scheda Amplia", + "desc": "Seleziona la scheda Amplia." + }, + "selectModelsTab": { + "title": "Seleziona la scheda Modelli", + "desc": "Seleziona la scheda Modelli." + }, + "selectQueueTab": { + "title": "Seleziona la scheda della Coda", + "desc": "Seleziona la scheda della Coda." + }, + "selectWorkflowsTab": { + "desc": "Seleziona la scheda dei Flussi di lavoro.", + "title": "Seleziona la scheda dei Flussi di lavoro" + }, + "focusPrompt": { + "title": "Seleziona il Prompt", + "desc": "Sposta il cursore sul prompt positivo." + }, + "toggleLeftPanel": { + "title": "Attiva/disattiva il pannello sinistro", + "desc": "Attiva/disattiva il pannello sinistro." + }, + "toggleRightPanel": { + "title": "Attiva/disattiva il pannello destro", + "desc": "Attiva/disattiva il pannello destro." + }, + "resetPanelLayout": { + "title": "Ripristina lo schema del pannello", + "desc": "Ripristina le dimensioni e lo schema predefiniti dei pannelli sinistro e destro." + }, + "togglePanels": { + "title": "Attiva/disattiva i pannelli", + "desc": "Mostra o nascondi contemporaneamente i pannelli sinistro e destro." + }, + "selectGenerateTab": { + "title": "Seleziona la scheda Genera", + "desc": "Seleziona la scheda Genera." + }, + "promptHistoryPrev": { + "title": "Prompt precedente nella cronologia", + "desc": "Quando il prompt è attivo, passa al prompt precedente (più vecchio) nella cronologia." + }, + "promptHistoryNext": { + "title": "Prossimo prompt nella cronologia", + "desc": "Quando il prompt è attivo, passa al prompt successivo (più recente) nella cronologia." + }, + "promptWeightUp": { + "title": "Aumenta il peso della selezione del prompt", + "desc": "Quando il prompt è attivo e il testo è selezionato, aumenta il peso del prompt selezionato." + }, + "promptWeightDown": { + "title": "Riduce il peso della selezione del prompt", + "desc": "Quando il prompt è attivo e il testo è selezionato, riduce il peso del prompt selezionato." + } + }, + "hotkeys": "Tasti di scelta rapida", + "canvas": { + "transformSelected": { + "desc": "Trasforma il livello selezionato.", + "title": "Trasforma" + }, + "fitBboxToCanvas": { + "desc": "Scala e posiziona la vista per adattarla al riquadro di delimitazione.", + "title": "Adatta il riquadro di delimitazione alla tela" + }, + "redo": { + "title": "Ripeti", + "desc": "Ripeti l'ultima azione sulla tela." + }, + "selectBrushTool": { + "title": "Strumento pennello", + "desc": "Seleziona lo strumento pennello." + }, + "selectBboxTool": { + "title": "Strumento di selezione riquadro", + "desc": "Seleziona lo strumento riquadro di delimitazione." + }, + "decrementToolWidth": { + "title": "Diminuisci la larghezza dello strumento", + "desc": "Diminuisce la larghezza dello strumento pennello o gomma, a seconda di quello selezionato." + }, + "incrementToolWidth": { + "title": "Aumenta la larghezza dello strumento", + "desc": "Aumenta la larghezza dello strumento pennello o gomma, a seconda di quello selezionato." + }, + "selectColorPickerTool": { + "title": "Strumento di selezione del colore", + "desc": "Seleziona lo strumento di selezione del colore." + }, + "resetSelected": { + "title": "Reimposta il Livello", + "desc": "Reimposta il livello selezionato. Si applica solo alla Maschera Inpaint e alla Guida Regionale." + }, + "undo": { + "title": "Annulla", + "desc": "Annulla l'ultima azione sulla tela." + }, + "nextEntity": { + "title": "Livello successivo", + "desc": "Seleziona il livello successivo nell'elenco." + }, + "filterSelected": { + "title": "Filtro", + "desc": "Filtra il livello selezionato. Applicabile solo ai livelli Raster e Controllo." + }, + "setZoomTo100Percent": { + "title": "Zoom al 100%", + "desc": "Imposta l'ingrandimento della tela al 100%." + }, + "setZoomTo200Percent": { + "title": "Zoom al 200%", + "desc": "Imposta l'ingrandimento della tela al 200%." + }, + "setZoomTo400Percent": { + "title": "Zoom al 400%", + "desc": "Imposta l'ingrandimento della tela al 400%." + }, + "setZoomTo800Percent": { + "title": "Zoom al 800%", + "desc": "Imposta l'ingrandimento della tela al 800%." + }, + "quickSwitch": { + "title": "Cambio rapido livello", + "desc": "Passa tra gli ultimi due livelli selezionati. Se un livello è aggiunto ai segnalibri, passa sempre tra questo e l'ultimo livello non aggiunto ai segnalibri." + }, + "deleteSelected": { + "title": "Elimina livello", + "desc": "Elimina il livello selezionato." + }, + "prevEntity": { + "title": "Livello precedente", + "desc": "Seleziona il livello precedente nell'elenco." + }, + "title": "Tela", + "selectMoveTool": { + "title": "Strumento Sposta", + "desc": "Seleziona lo strumento sposta." + }, + "fitLayersToCanvas": { + "desc": "Scala e posiziona la vista per adattarla a tutti i livelli visibili.", + "title": "Adatta i livelli alla tela" + }, + "selectEraserTool": { + "title": "Strumento gomma", + "desc": "Selezionare lo strumento gomma." + }, + "selectRectTool": { + "title": "Strumento Rettangolo", + "desc": "Seleziona lo strumento rettangolo." + }, + "selectViewTool": { + "title": "Strumento Visualizza", + "desc": "Seleziona lo strumento Visualizza." + }, + "applyFilter": { + "title": "Applica filtro", + "desc": "Applica il filtro in sospeso al livello selezionato." + }, + "cancelFilter": { + "title": "Annulla filtro", + "desc": "Annulla il filtro in sospeso." + }, + "cancelTransform": { + "desc": "Annulla la trasformazione in sospeso.", + "title": "Annulla Trasforma" + }, + "applyTransform": { + "title": "Applica trasformazione", + "desc": "Applica la trasformazione in sospeso al livello selezionato." + }, + "toggleNonRasterLayers": { + "desc": "Mostra o nascondi tutte le categorie di livelli non raster (Livelli di controllo, Maschere di Inpaint, Guida regionale).", + "title": "Attiva/disattiva livelli non raster" + }, + "settings": { + "behavior": "Comportamento", + "display": "Mostra", + "grid": "Griglia" + }, + "invertMask": { + "title": "Inverti maschera", + "desc": "Inverte la maschera di inpaint selezionata, creando una nuova maschera con trasparenza opposta." + }, + "fitBboxToMasks": { + "title": "Adatta il riquadro di delimitazione alle maschere", + "desc": "Regola automaticamente il riquadro di delimitazione della generazione per adattarlo alle maschere di inpaint visibili" + }, + "applySegmentAnything": { + "title": "Applica Segment Anything", + "desc": "Applica la maschera Segment Anything corrente.", + "key": "invio" + }, + "cancelSegmentAnything": { + "title": "Annulla Segment Anything", + "desc": "Annulla l'operazione Segment Anything corrente." + }, + "fitBboxToLayers": { + "title": "Adatta il riquadro di delimitazione ai livelli", + "desc": "Regola automaticamente il riquadro di delimitazione della generazione per adattarlo ai livelli visibili" + }, + "toggleBbox": { + "title": "Attiva/disattiva la visibilità del riquadro di delimitazione", + "desc": "Nascondi o mostra il riquadro di delimitazione della generazione" + }, + "setFillColorsToDefault": { + "title": "Imposta i colori come predefiniti", + "desc": "Imposta i colori degli strumenti correnti sui valori predefiniti." + }, + "toggleFillColor": { + "title": "Attiva/disattiva colore di riempimento", + "desc": "Attiva/disattiva il colore di riempimento dello strumento corrente." + }, + "selectLassoTool": { + "title": "Strumento Lazo", + "desc": "Seleziona lo strumento lazo." + }, + "mergeDown": { + "title": "Unisci livello verso il basso", + "desc": "Unisci il livello selezionato al livello immediatamente sottostante." + }, + "mergeVisible": { + "title": "Unisci tutti i livelli visibili", + "desc": "Unisci tutti i livelli visibili del tipo di livello selezionato." + } + }, + "workflows": { + "addNode": { + "title": "Aggiungi nodo", + "desc": "Apri il menu aggiungi nodo." + }, + "pasteSelectionWithEdges": { + "title": "Incolla con collegamenti", + "desc": "Incolla i nodi copiati, i collegamenti e tutti i collegamenti connessi ai nodi copiati." + }, + "copySelection": { + "title": "Copia", + "desc": "Copia i nodi ed i collegamenti selezionati." + }, + "pasteSelection": { + "title": "Incolla", + "desc": "Incolla i nodi ed i collegamenti copiati." + }, + "deleteSelection": { + "title": "Elimina", + "desc": "Elimina i nodi ed i collegamenti selezionati." + }, + "redo": { + "title": "Ripeti", + "desc": "Ripeti l'ultima azione del flusso di lavoro." + }, + "selectAll": { + "desc": "Seleziona tutti i nodi ed i collegamenti.", + "title": "Seleziona tutto" + }, + "undo": { + "desc": "Annulla l'ultima azione del flusso di lavoro.", + "title": "Annulla" + }, + "title": "Flussi di lavoro" + }, + "viewer": { + "nextComparisonMode": { + "title": "Modalità di confronto successiva", + "desc": "Scorri le modalità di confronto." + }, + "recallPrompts": { + "title": "Richiama i Prompt", + "desc": "Richiama i prompt positivo e negativo per l'immagine corrente." + }, + "remix": { + "title": "Remixa", + "desc": "Richiama tutti i metadati, ad eccezione del seme, per l'immagine corrente." + }, + "useSize": { + "desc": "Utilizza la dimensione dell'immagine corrente come dimensione del riquadro di delimitazione.", + "title": "Usa Dimensioni" + }, + "runPostprocessing": { + "title": "Esegui Post-elaborazione", + "desc": "Esegue la post-elaborazione selezionata sull'immagine corrente." + }, + "title": "Visualizzatore immagini", + "toggleViewer": { + "title": "Mostra/Nascondi visualizzatore immagini", + "desc": "Mostra o nascondi il visualizzatore di immagini. Disponibile solo nella scheda Tela." + }, + "loadWorkflow": { + "title": "Carica Flusso di lavoro", + "desc": "Carica il flusso di lavoro salvato dell'immagine corrente (se presente)." + }, + "recallAll": { + "title": "Richiama tutti i metadati", + "desc": "Richiama tutti i metadati dell'immagine corrente." + }, + "swapImages": { + "title": "Scambia le immagini di confronto", + "desc": "Scambia le immagini da confrontare." + }, + "recallSeed": { + "title": "Richiama il seme", + "desc": "Richiama il seme per l'immagine corrente." + }, + "toggleMetadata": { + "title": "Mostra/Nascondi metadati", + "desc": "Mostra o nasconde la sovrapposizione dei metadati dell'immagine corrente." + } + }, + "gallery": { + "selectAllOnPage": { + "desc": "Seleziona tutte le immagini nella pagina corrente.", + "title": "Seleziona tutto nella pagina" + }, + "galleryNavUp": { + "desc": "Naviga verso l'alto nella griglia della galleria, selezionando quell'immagine. Se sei in cima alla pagina, andrai alla pagina precedente.", + "title": "Naviga verso l'alto" + }, + "galleryNavRight": { + "title": "Naviga a destra", + "desc": "Naviga a destra nella griglia della galleria, selezionando quell'immagine. Se sei all'ultima immagine della riga, andrai alla riga successiva. Se sei all'ultima immagine della pagina, andrai alla pagina successiva." + }, + "galleryNavLeftAlt": { + "desc": "Uguale a Naviga a sinistra, ma seleziona l'immagine da confrontare, aprendo la modalità di confronto se non è già aperta.", + "title": "Naviga a sinistra (Confronta immagine)" + }, + "deleteSelection": { + "title": "Elimina", + "desc": "Elimina tutte le immagini selezionate. Per impostazione predefinita, ti verrà chiesto di confermare l'eliminazione. Se le immagini sono attualmente in uso nell'applicazione, verrai avvisato." + }, + "clearSelection": { + "title": "Cancella selezione", + "desc": "Cancella la selezione corrente, se presente." + }, + "galleryNavRightAlt": { + "desc": "Uguale a Naviga a destra, ma seleziona l'immagine da confrontare, aprendo la modalità di confronto se non è già aperta.", + "title": "Naviga a destra (Confronta immagine)" + }, + "galleryNavDownAlt": { + "title": "Naviga in basso (Confronta immagine)", + "desc": "Uguale a Naviga in basso, ma seleziona l'immagine da confrontare, aprendo la modalità di confronto se non è già aperta." + }, + "title": "Galleria", + "galleryNavDown": { + "desc": "Naviga verso il basso nella griglia della galleria, selezionando quell'immagine. Se sei in fondo alla pagina, andrai alla pagina successiva.", + "title": "Naviga in basso" + }, + "galleryNavLeft": { + "title": "Naviga a sinistra", + "desc": "Naviga a sinistra nella griglia della galleria, selezionando quell'immagine. Se sei alla prima immagine della riga, andrai alla riga precedente. Se sei alla prima immagine della pagina, andrai alla pagina precedente." + }, + "galleryNavUpAlt": { + "desc": "Uguale a Naviga verso l'alto, ma seleziona l'immagine da confrontare, aprendo la modalità di confronto se non è già aperta.", + "title": "Naviga verso l'alto (Confronta immagine)" + }, + "starImage": { + "desc": "Aggiungi/Rimuovi contrassegno all'immagine selezionata.", + "title": "Aggiungi / Rimuovi contrassegno immagine" + } + }, + "editMode": "Modalità modifica", + "viewMode": "Modalità visualizzazione", + "editHotkey": "Modifica tasto di scelta rapida", + "addHotkey": "Aggiungi tasto di scelta rapida", + "resetToDefault": "Ripristina predefinito", + "resetAll": "Ripristina tutto ai predefiniti", + "resetAllConfirmation": "Vuoi davvero ripristinare tutti i tasti di scelta rapida ai valori predefiniti? Questa operazione non può essere annullata.", + "enterHotkeys": "Inserisci i tasti di scelta rapida, separati da virgole", + "save": "Salva", + "cancel": "Annulla", + "modifiers": "Modificatori", + "syntaxHelp": "Guida alla sintassi", + "combineWith": "Combina con +", + "multipleHotkeys": "Tasti di scelta rapida multipli con virgola", + "validKeys": "Tasti validi", + "help": "Aiuto", + "noHotkeysRecorded": "Nessun tasto di scelta rapida registrato ancora", + "pressKeys": "Premi i tasti...", + "setHotkey": "Imposta", + "setAnother": "Imposta un'altro", + "removeLastHotkey": "Rimuovi l'ultimo tasto di scelta rapida", + "clearAll": "Cancella tutto", + "duplicateWarning": "Questo tasto di scelta rapida è già registrato", + "conflictWarning": "è già utilizzato da \"{{hotkeyTitle}}\"", + "thisHotkey": "questo tasto di scelta rapida" + }, + "modelManager": { + "modelManager": "Gestione Modelli", + "model": "Modello", + "allModels": "Tutti i modelli", + "modelUpdated": "Modello aggiornato", + "manual": "Manuale", + "name": "Nome", + "description": "Descrizione", + "config": "Configurazione", + "repo_id": "Repo ID", + "width": "Larghezza", + "height": "Altezza", + "addModel": "Aggiungi modello", + "availableModels": "Modelli disponibili", + "search": "Ricerca", + "load": "Carica", + "active": "attivo", + "selected": "Selezionato", + "delete": "Elimina", + "deleteModel": "Elimina modello", + "deleteConfig": "Elimina configurazione", + "deleteMsg1": "Sei sicuro di voler eliminare questo modello da InvokeAI?", + "deleteMsg2": "Questo eliminerà il modello dal disco se si trova nella cartella principale di InvokeAI. Se invece utilizzi una cartella personalizzata, il modello NON verrà eliminato dal disco.", + "convert": "Converti", + "convertToDiffusers": "Converti in Diffusori", + "convertToDiffusersHelpText2": "Questo processo sostituirà la voce in Gestione Modelli con la versione Diffusori dello stesso modello.", + "convertToDiffusersHelpText4": "Questo è un processo una tantum. Potrebbero essere necessari circa 30-60 secondi a seconda delle specifiche del tuo computer.", + "convertToDiffusersHelpText5": "Assicurati di avere spazio su disco sufficiente. I modelli generalmente variano tra 2 GB e 7 GB in dimensione.", + "convertToDiffusersHelpText6": "Vuoi convertire questo modello?", + "modelConverted": "Modello convertito", + "alpha": "Alpha", + "convertToDiffusersHelpText1": "Questo modello verrà convertito nel formato 🧨 Diffusori.", + "convertToDiffusersHelpText3": "Il file del modello su disco verrà eliminato se si trova nella cartella principale di InvokeAI. Se si trova invece in una posizione personalizzata, NON verrà eliminato.", + "none": "nessuno", + "variant": "Variante", + "baseModel": "Modello Base", + "vae": "VAE", + "modelUpdateFailed": "Aggiornamento del modello non riuscito", + "modelConversionFailed": "Conversione del modello non riuscita", + "selectModel": "Seleziona Modello", + "modelDeleted": "Modello eliminato", + "modelDeleteFailed": "Impossibile eliminare il modello", + "convertingModelBegin": "Conversione del modello. Attendere prego.", + "settings": "Impostazioni", + "syncModels": "Sincronizza modelli", + "predictionType": "Tipo di previsione", + "advanced": "Avanzate", + "modelType": "Tipo di modello", + "vaePrecision": "Precisione VAE", + "noModelSelected": "Nessun modello selezionato", + "modelName": "Nome del modello", + "modelSettings": "Impostazioni del modello", + "addModels": "Aggiungi modelli", + "cancel": "Annulla", + "edit": "Modifica", + "imageEncoderModelId": "ID modello codificatore di immagini", + "path": "Percorso", + "prune": "Elimina", + "pruneTooltip": "Elimina dalla coda le importazioni completate", + "repoVariant": "Variante del repository", + "scanFolder": "Scansione cartella", + "scanResults": "Risultati della scansione", + "source": "Sorgente", + "upcastAttention": "Eleva l'attenzione", + "typePhraseHere": "Digita la frase qui", + "defaultSettingsSaved": "Impostazioni predefinite salvate", + "defaultSettings": "Impostazioni predefinite", + "metadata": "Metadati", + "triggerPhrases": "Frasi Trigger", + "deleteModelImage": "Elimina l'immagine del modello", + "localOnly": "solo locale", + "modelImageDeleted": "Immagine del modello eliminata", + "modelImageDeleteFailed": "Eliminazione dell'immagine del modello non riuscita", + "modelImageUpdated": "Immagine del modello aggiornata", + "modelImageUpdateFailed": "Aggiornamento dell'immagine del modello non riuscito", + "pathToConfig": "Percorso file di configurazione", + "uploadImage": "Carica immagine", + "loraTriggerPhrases": "Frasi Trigger LoRA", + "mainModelTriggerPhrases": "Frasi Trigger del modello principale", + "inplaceInstall": "Installazione sul posto", + "inplaceInstallDesc": "Installa i modelli senza spostare i file. Quando si utilizza il modello, verrà caricato dalla posizione originale. Se disabilitato, i file del modello verranno spostati nella directory dei modelli gestiti da Invoke durante l'installazione.", + "installQueue": "Coda di installazione", + "install": "Installa", + "installRepo": "Installa Repository", + "huggingFacePlaceholder": "proprietario/nome-modello", + "huggingFaceHelper": "Se in questo repository vengono trovati più modelli, ti verrà richiesto di selezionarne uno da installare.", + "installAll": "Installa tutto", + "scanFolderHelper": "La cartella verrà sottoposta a scansione ricorsiva per i modelli. L'operazione può richiedere un po' di tempo per cartelle molto grandi.", + "scanPlaceholder": "Percorso di una cartella locale", + "simpleModelPlaceholder": "URL o percorso di un file locale o di una cartella diffusori", + "urlOrLocalPath": "URL o percorso locale", + "urlOrLocalPathHelper": "Gli URL dovrebbero puntare a un singolo file. I percorsi locali possono puntare a un singolo file o cartella per un singolo modello di diffusore.", + "loraModels": "LoRA", + "starterModels": "Modelli iniziali", + "textualInversions": "Inversioni Testuali", + "noModelsInstalled": "Nessun modello installato", + "main": "Principali", + "noModelsInstalledDesc1": "Installa i modelli con", + "noMatchingModels": "Nessun modello corrispondente", + "spandrelImageToImage": "Immagine a immagine (Spandrel)", + "learnMoreAboutSupportedModels": "Scopri di più sui modelli che supportiamo", + "starterBundles": "Pacchetti per iniziare", + "installingBundle": "Installazione del pacchetto", + "skippingXDuplicates_one": ", saltando {{count}} duplicato", + "skippingXDuplicates_many": ", saltando {{count}} duplicati", + "skippingXDuplicates_other": ", saltando {{count}} duplicati", + "installingModel": "Installazione del modello", + "installingXModels_one": "Installazione di {{count}} modello", + "installingXModels_many": "Installazione di {{count}} modelli", + "installingXModels_other": "Installazione di {{count}} modelli", + "includesNModels": "Include {{n}} modelli e le loro dipendenze.", + "starterBundleHelpText": "Installa facilmente tutti i modelli necessari per iniziare con un modello base, tra cui un modello principale, controlnet, adattatori IP e altro. Selezionando un pacchetto salterai tutti i modelli che hai già installato.", + "noDefaultSettings": "Nessuna impostazione predefinita configurata per questo modello. Visita Gestione Modelli per aggiungere impostazioni predefinite.", + "defaultSettingsOutOfSync": "Alcune impostazioni non corrispondono a quelle predefinite del modello:", + "restoreDefaultSettings": "Fare clic per utilizzare le impostazioni predefinite del modello.", + "usingDefaultSettings": "Utilizzo delle impostazioni predefinite del modello", + "huggingFace": "HuggingFace", + "huggingFaceRepoID": "HuggingFace Repository ID", + "clipEmbed": "CLIP Embed", + "t5Encoder": "T5 Encoder", + "hfTokenInvalidErrorMessage": "Gettone HuggingFace non valido o mancante.", + "hfTokenRequired": "Stai tentando di scaricare un modello che richiede un gettone HuggingFace valido.", + "hfTokenUnableToVerifyErrorMessage": "Impossibile verificare il gettone HuggingFace. Ciò è probabilmente dovuto a un errore di rete. Riprova più tardi.", + "hfTokenHelperText": "Per utilizzare alcuni modelli è necessario un gettone HF. Fai clic qui per creare o ottenere il tuo gettone.", + "hfTokenInvalid": "Gettone HF non valido o mancante", + "hfTokenUnableToVerify": "Impossibile verificare il gettone HF", + "hfTokenSaved": "Gettone HF salvato", + "hfForbidden": "Non hai accesso a questo modello HF", + "hfTokenLabel": "Gettone HuggingFace (richiesto per alcuni modelli)", + "hfForbiddenErrorMessage": "Consigliamo di visitare la pagina del repository. Il proprietario potrebbe richiedere l'accettazione dei termini per poter effettuare il download.", + "hfTokenInvalidErrorMessage2": "Aggiornalo in ", + "controlLora": "Controllo LoRA", + "urlUnauthorizedErrorMessage2": "Scopri come qui.", + "urlForbidden": "Non hai accesso a questo modello", + "urlForbiddenErrorMessage": "Potrebbe essere necessario richiedere l'autorizzazione al sito che distribuisce il modello.", + "urlUnauthorizedErrorMessage": "Potrebbe essere necessario configurare un gettone API per accedere a questo modello.", + "fileSize": "Dimensione del file", + "modelPickerFallbackNoModelsInstalled": "Nessun modello installato.", + "modelPickerFallbackNoModelsInstalled2": "Visita Gestione modelli per installare i modelli.", + "manageModels": "Gestione modelli", + "hfTokenReset": "Ripristino del gettone HF", + "relatedModels": "Modelli correlati", + "installedModelsCount": "{{installed}} di {{total}} modelli installati.", + "allNModelsInstalled": "Tutti i {{count}} modelli installati", + "nToInstall": "{{count}} da installare", + "nAlreadyInstalled": "{{count}} già installati", + "bundleAlreadyInstalled": "Pacchetto già installato", + "bundleAlreadyInstalledDesc": "Tutti i modelli nel pacchetto {{bundleName}} sono già installati.", + "launchpad": { + "description": "Per utilizzare la maggior parte delle funzionalità della piattaforma, Invoke richiede l'installazione di modelli. Scegli tra le opzioni di installazione manuale o esplora i modelli di avvio selezionati.", + "manualInstall": "Installazione manuale", + "urlDescription": "Installa i modelli da un URL o da un percorso file locale. Perfetto per modelli specifici che desideri aggiungere.", + "huggingFaceDescription": "Esplora e installa i modelli direttamente dai repository di HuggingFace.", + "scanFolderDescription": "Esegui la scansione di una cartella locale per rilevare e installare automaticamente i modelli.", + "recommendedModels": "Modelli consigliati", + "exploreStarter": "Oppure sfoglia tutti i modelli iniziali disponibili", + "welcome": "Benvenuti in Gestione Modelli", + "bundleDescription": "Ogni pacchetto include modelli essenziali per ogni famiglia di modelli e modelli base selezionati per iniziare.", + "quickStart": "Pacchetti di avvio rapido", + "browseAll": "Oppure sfoglia tutti i modelli disponibili:", + "externalDescription": "Collega una chiave API Gemini o OpenAI per abilitare la generazione esterna. L'utilizzo potrebbe comportare costi da parte del fornitore." + }, + "launchpadTab": "Rampa di lancio", + "installBundle": "Installa pacchetto", + "installBundleMsg1": "Vuoi davvero installare il pacchetto {{bundleName}}?", + "installBundleMsg2": "Questo pacchetto installerà i seguenti {{count}} modelli:", + "filterModels": "Filtra i modelli", + "ipAdapters": "Adattatori IP", + "showOnlyRelatedModels": "Correlati", + "starterModelsInModelManager": "I modelli di avvio possono essere trovati in Gestione Modelli", + "unidentifiedModelTitle": "Impossibile identificare il modello", + "unidentifiedModelMessage": "Non siamo riusciti a identificare il tipo, la base e/o il formato del modello installato. Prova a modificare il modello e a selezionare le impostazioni appropriate.", + "unidentifiedModelMessage2": "Se non vedi le impostazioni corrette o il modello non funziona dopo averle modificate, chiedi aiuto su o crea un problema su .", + "modelFormat": "Formato del modello", + "modelSettingsWarning": "Queste impostazioni indicano a Invoke di che tipo di modello si tratta e come caricarlo. Se Invoke non le ha rilevate correttamente durante l'installazione del modello, o se il modello è classificato come Sconosciuto, potrebbe essere necessario modificarle manualmente.", + "reidentify": "Reidentificare", + "reidentifyTooltip": "Se un modello non è stato installato correttamente (ad esempio, ha il tipo sbagliato o non funziona), puoi provare a identificarlo nuovamente. Questo reimposterà tutte le impostazioni personalizzate che potresti aver applicato.", + "reidentifySuccess": "Modello reidentificato con successo", + "reidentifyUnknown": "Impossibile identificare il modello", + "reidentifyError": "Errore durante la reidentificazione del modello", + "flux2KleinQwen3EncoderPlaceholder": "Dal modello diffusori", + "flux2KleinQwen3Encoder": "Encoder Qwen3 (opzionale)", + "flux2KleinVaePlaceholder": "Dal modello diffusori", + "flux2KleinVae": "VAE (opzionale)", + "zImageQwen3SourcePlaceholder": "Obbligatorio se VAE/Encoder è vuoto", + "zImageQwen3Source": "Modello sorgente Qwen3 e VAE", + "zImageQwen3EncoderPlaceholder": "Dal modello sorgente Qwen3", + "zImageQwen3Encoder": "Encoder Qwen3 (opzionale)", + "zImageVaePlaceholder": "Dal modello sorgente VAE", + "qwen3Encoder": "Encoder Quen3", + "selectAll": "Seleziona tutto", + "deleteModels": "Elimina modelli", + "invalidPathFormat": "Il percorso deve essere un percorso assoluto (ad esempio, C:\\Models\\... o /home/user/models/...)", + "pathUpdateFailed": "Impossibile aggiornare il percorso del modello", + "pathUpdated": "Percorso del modello aggiornato correttamente", + "newPathPlaceholder": "Inserisci un nuovo percorso...", + "newPath": "Nuovo percorso", + "currentPath": "Percorso attuale", + "updatePathDescription": "Inserisci il nuovo percorso del file o della directory del modello. Utilizza questo percorso se hai spostato manualmente i file del modello sul disco.", + "updatePathTooltip": "Aggiorna il percorso del file per questo modello se hai spostato i file del modello in una nuova posizione.", + "updatePath": "Aggiorna percorso", + "actions": "Azioni in blocco", + "zImageVae": "VAE (opzionale)", + "missingFiles": "File mancanti", + "missingFilesTooltip": "I file del modello sono mancanti dal disco", + "cpuOnly": "Solo CPU", + "runOnCpu": "Esegui il modello di codifica del testo solo sulla CPU", + "syncModelsTooltip": "Identificare e rimuovere i file modello non utilizzati nella cartella radice di InvokeAI.", + "syncModelsDirectory": "Sincronizza la cartella dei modelli", + "noOrphanedModels": "La cartella dei modelli è sincronizzata. Nessun file orfano trovato.", + "orphanedModelsFound": "Modelli orfani trovati", + "orphanedModelsDescription": "Le seguenti cartelle dei modelli non sono referenziate nel database e possono essere eliminate in sicurezza:", + "foundOrphanedModels_one": "Trovata {{count}} cartella di modello orfana", + "foundOrphanedModels_many": "", + "foundOrphanedModels_other": "", + "filesCount": "{{count}} file", + "deleteSelected": "Elimina {{count}} selezionati", + "deselectAll": "Deseleziona tutto", + "orphanedModelsDeleted": "Eliminato con successo {{count}} modello orfano", + "orphanedModelsDeleteErrors": "Alcuni modelli non possono essere eliminati", + "orphanedModelsDeleteFailed": "Impossibile eliminare i modelli orfani", + "errorLoadingOrphanedModels": "Errore durante il caricamento dei modelli orfani. Riprova.", + "pause": "Pausa", + "pauseAll": "Metti in pausa tutto", + "pauseAllTooltip": "Metti in pausa tutti i download attivi", + "resume": "Riprendi", + "resumeAll": "Riprendi tutto", + "resumeAllTooltip": "Riprendi tutti i download in pausa", + "restartFailed": "Riavvio non riuscito", + "restartFile": "Riavvia il file", + "restartRequired": "Riavvio richiesto", + "resumeRefused": "Ripristino rifiutato dal server. Riavvio richiesto.", + "backendDisconnected": "Backend disconnesso", + "cancelAll": "Annulla tutto", + "cancelAllTooltip": "Annulla tutti i download attivi", + "selectModelToView": "Seleziona un modello per visualizzarne i dettagli", + "exportSettings": "Impostazioni di esportazione", + "importSettings": "Impostazioni di importazione", + "settingsExported": "Impostazioni del modello esportate", + "settingsImported": "Impostazioni del modello importate", + "settingsImportedPartial": "Impostazioni del modello parzialmente importate. Le impostazioni incompatibili sono state ignorate: {{fields}}", + "settingsImportFailed": "Impossibile importare le impostazioni del modello", + "settingsImportIncompatible": "Il file delle impostazioni non contiene impostazioni compatibili per questo tipo di modello", + "settingsImportInvalidFile": "File di impostazioni non valido", + "reidentifyModels": "Re-identificare i modelli", + "reidentifyModelsConfirm": "Sei sicuro di voler re-identificare {{count}} modello(i)? Questa operazione eseguirà una nuova scansione dei relativi file dei pesi per determinarne il formato e le impostazioni corrette.", + "reidentifyWarning": "Questa operazione ripristinerà tutte le impostazioni personalizzate che potresti aver applicato a questi modelli.", + "modelsReidentified": "{{count}} modello(i) re-identificato(i) con successo", + "modelsReidentifyFailed": "Impossibile re-identificare i modelli", + "someModelsFailedToReidentify": "Non è stato possibile re-identificare {{count}} modello(i)", + "modelsReidentifiedPartial": "Completato parzialmente", + "someModelsReidentified": "{{succeeded}} re-identificato(i), {{failed}} fallito(i)", + "modelsReidentifyError": "Errore nella re-identificazione dei modelli", + "deleteModelsConfirm": "Sei sicuro di voler eliminare {{count}} modello(i)? Questa azione non può essere annullata.", + "deleteWarning": "I modelli presenti nella cartella dei modelli di Invoke verranno eliminati definitivamente dal disco.", + "modelsDeleted": "{{count}} modello(i) eliminato(i) con successo", + "modelsDeleteFailed": "Impossibile eliminare i modelli", + "someModelsFailedToDelete": "Non è stato possibile eliminare {{count}} modello(i)", + "modelsDeletedPartial": "Parzialmente completato", + "someModelsDeleted": "{{deleted}} eliminato(i), {{failed}} fallito(i)", + "modelsDeleteError": "Errore durante l'eliminazione dei modelli", + "queueEmpty": "La coda di installazione è vuota.", + "animaVaePlaceholder": "Seleziona VAE compatibile con Anima", + "animaQwen3EncoderPlaceholder": "Seleziona l'encoder Qwen3 0.6B", + "animaT5EncoderPlaceholder": "Seleziona l'encoder T5-XXL", + "qwenImageComponentSourcePlaceholder": "Necessario per i modelli GGUF", + "qwenImageComponentSource": "VAE/Sorgente Encoder (Diffusori)", + "qwenImageQuantization": "Quantizzazione dell'encoder", + "qwenImageQuantizationNone": "Nessuna (bf16)", + "modelPickerFallbackNoModelsInstalledNonAdmin": "Nessun modello installato. Chiedi al tuo amministratore di InvokeAI () di installare alcuni modelli.", + "noModelsInstalledAskAdmin": "Chiedi al tuo amministratore di installarne alcuni.", + "externalImageGenerator": "Generatore di immagini esterno", + "externalProviders": "Fornitori esterni", + "externalSetupTitle": "Configurazione dei fornitori esterni", + "externalSetupDescription": "Collega una chiave API per abilitare la generazione di immagini esterne. I modelli di avvio esterni vengono installati automaticamente quando viene configurato un provider.", + "externalInstallDefaults": "Modelli di avviamento ad installazione automatica", + "externalProvidersUnavailable": "In questa versione non sono supportati i provider esterni.", + "externalSetupFooter": "È necessaria una chiave API. I fornitori esterni utilizzano API remote; l'utilizzo potrebbe comportare costi a carico del fornitore.", + "externalProviderCardDescription": "Configura le credenziali {{providerId}} per la generazione di immagini esterne.", + "externalApiKey": "Chiave API", + "externalApiKeyPlaceholder": "Incolla la tua chiave API", + "externalApiKeyPlaceholderSet": "Chiave API configurata", + "externalApiKeyHelper": "Memorizzato nel file di configurazione di InvokeAI.", + "externalBaseUrl": "URL di base (facoltativo)", + "externalBaseUrlHelper": "Se necessario, sovrascrivi l'URL di base predefinito dell'API.", + "externalResetHelper": "Cancella la chiave API e l'URL di base.", + "sortByName": "Nome", + "sortBySize": "Dimensione", + "sortByDateAdded": "Data di aggiunta", + "sortByDateModified": "Data di modifica", + "sortByPath": "Percorso", + "sortByType": "Tipo", + "sortByFormat": "Formato", + "sortDefault": "Predefinito", + "externalProvider": "Fornitore esterno", + "externalCapabilities": "Capacità", + "externalDefaults": "Impostazioni predefinite", + "providerId": "ID Fornitore", + "providerModelId": "ID modello del fornitore", + "supportedModes": "Modalità supportate", + "supportsNegativePrompt": "Supporta il prompt negativo", + "supportsReferenceImages": "Supporta immagini di riferimento", + "supportsSeed": "Supporta il Seme", + "supportsGuidance": "Supporta la guida", + "maxImagesPerRequest": "Numero massimo di immagini per richiesta", + "maxReferenceImages": "Numero massimo di immagini di riferimento", + "maxImageWidth": "Larghezza massima immagine", + "flux2KleinVaeNoModelPlaceholder": "Nessun modello diffusori disponibile", + "flux2KleinQwen3EncoderNoModelPlaceholder": "Nessun modello diffusori disponibile", + "maxImageHeight": "Altezza massima dell'immagine", + "numImages": "Numero di immagini", + "textLLM": "LLM testuale", + "sourceUrl": "URL di origine", + "fp8Storage": "Archiviazione FP8 (Risparmia VRAM)", + "qwenImageVaePlaceholder": "Dalla sorgente VAE/Encoder", + "qwenImageQwenVLEncoderPlaceholder": "Dalla sorgente VAE/Encoder" + }, + "parameters": { + "images": "Immagini", + "steps": "Passi", + "cfgScale": "Scala CFG", + "width": "Larghezza", + "height": "Altezza", + "seed": "Seme", + "shuffle": "Mescola il seme", + "noiseThreshold": "Soglia del rumore", + "perlinNoise": "Rumore Perlin", + "type": "Tipo", + "strength": "Forza", + "upscaling": "Amplia", + "scale": "Scala", + "imageFit": "Adatta l'immagine iniziale alle dimensioni di output", + "scaleBeforeProcessing": "Scala prima dell'elaborazione", + "scaledWidth": "Larghezza scalata", + "scaledHeight": "Altezza scalata", + "infillMethod": "Metodo di riempimento", + "tileSize": "Dimensione piastrella", + "usePrompt": "Usa Prompt", + "useSeed": "Usa Seme", + "useAll": "Usa Tutto", + "info": "Informazioni", + "general": "Generale", + "denoisingStrength": "Forza di riduzione del rumore", + "copyImage": "Copia immagine", + "cancel": { + "cancel": "Annulla" + }, + "symmetry": "Simmetria", + "seamlessXAxis": "Asse X senza giunte", + "seamlessYAxis": "Asse Y senza giunte", + "scheduler": "Campionatore", + "positivePromptPlaceholder": "Prompt Positivo", + "negativePromptPlaceholder": "Prompt Negativo", + "controlNetControlMode": "Modalità di controllo", + "clipSkip": "CLIP Skip", + "maskBlur": "Sfoc. maschera", + "patchmatchDownScaleSize": "Ridimensiona", + "coherenceMode": "Modalità", + "invoke": { + "noNodesInGraph": "Nessun nodo nel grafico", + "noModelSelected": "Nessun modello selezionato", + "noPrompts": "Nessun prompt generato", + "addingImagesTo": "Aggiungi immagini a", + "systemDisconnected": "Sistema disconnesso", + "missingNodeTemplate": "Modello di nodo mancante", + "missingInputForField": "ingresso mancante", + "missingFieldTemplate": "Modello di campo mancante", + "noT5EncoderModelSelected": "Nessun modello di encoder T5 selezionato per la generazione con FLUX", + "noCLIPEmbedModelSelected": "Nessun modello CLIP Embed selezionato per la generazione con FLUX", + "noFLUXVAEModelSelected": "Nessun modello VAE selezionato per la generazione con FLUX", + "canvasIsTransforming": "La tela è occupata (sta trasformando)", + "canvasIsRasterizing": "La tela è occupata (sta rasterizzando)", + "canvasIsCompositing": "La tela è occupata (in composizione)", + "canvasIsFiltering": "La tela è occupata (sta filtrando)", + "collectionTooManyItems": "troppi elementi, massimo {{maxItems}}", + "canvasIsSelectingObject": "La tela è occupata (selezione dell'oggetto)", + "collectionTooFewItems": "troppi pochi elementi, minimo {{minItems}}", + "fluxModelMultipleControlLoRAs": "È possibile utilizzare solo 1 Controllo LoRA alla volta", + "collectionNumberGTMax": "{{value}} > {{maximum}} (incr max)", + "collectionStringTooLong": "troppo lungo, massimo {{maxLength}}", + "batchNodeNotConnected": "Nodo Lotto non connesso: {{label}}", + "batchNodeEmptyCollection": "Alcuni nodi lotto hanno raccolte vuote", + "batchNodeCollectionSizeMismatch": "Le dimensioni della raccolta nel Lotto {{batchGroupId}} non corrispondono", + "collectionStringTooShort": "troppo corto, minimo {{minLength}}", + "collectionNumberNotMultipleOf": "{{value}} non è multiplo di {{multipleOf}}", + "collectionNumberLTMin": "{{value}} < {{minimum}} (incr min)", + "collectionNumberGTExclusiveMax": "{{value}} >= {{exclusiveMaximum}} (excl max)", + "collectionNumberLTExclusiveMin": "{{value}} <= {{exclusiveMinimum}} (excl min)", + "collectionEmpty": "raccolta vuota", + "batchNodeCollectionSizeMismatchNoGroupId": "Dimensione della raccolta di gruppo nel Lotto non corrisponde", + "modelIncompatibleBboxWidth": "La larghezza del riquadro di delimitazione è {{width}} ma {{model}} richiede multipli di {{multiple}}", + "modelIncompatibleBboxHeight": "L'altezza del riquadro è {{height}} ma {{model}} richiede multipli di {{multiple}}", + "modelIncompatibleScaledBboxWidth": "La larghezza scalata del riquadro è {{width}} ma {{model}} richiede multipli di {{multiple}}", + "modelIncompatibleScaledBboxHeight": "L'altezza scalata del riquadro è {{height}} ma {{model}} richiede multipli di {{multiple}}", + "modelDisabledForTrial": "La generazione con {{modelName}} non è disponibile per gli account di prova. Accedi alle impostazioni del tuo account per effettuare l'upgrade.", + "promptExpansionResultPending": "Accetta o ignora il risultato dell'espansione del prompt", + "promptExpansionPending": "Espansione del prompt in corso", + "noStartingFrameImage": "Nessuna immagine del fotogramma iniziale", + "incompatibleLoRAs": "Aggiunti LoRA incompatibili", + "emptyBatches": "lotti vuoti", + "fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), la larghezza del riquadro è {{width}}", + "fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), l'altezza del riquadro è {{height}}", + "fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), la larghezza ridimensionata del riquadro è {{width}}", + "fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), l'altezza ridimensionata del riquadro è {{height}}", + "noZImageQwen3EncoderSourceSelected": "Nessuna sorgente Qwen3 Encoder: seleziona il modello Qwen3 Encoder o Qwen3 Source", + "noZImageVaeSourceSelected": "Nessuna sorgente VAE: selezionare il modello di sorgente VAE (FLUX) o Qwen3", + "noQwen3EncoderModelSelected": "Nessun modello di encoder Qwen3 selezionato per la generazione Klein di FLUX2", + "noAnimaVaeModelSelected": "Nessun modello VAE Anima selezionato", + "noAnimaQwen3EncoderModelSelected": "Nessun modello di encoder Anima Qwen3 selezionato", + "noAnimaT5EncoderModelSelected": "Nessun modello di encoder Anima T5 selezionato", + "noQwenImageComponentSourceSelected": "I modelli GGUF Qwen Image richiedono una sorgente componente diffusori per VAE/encoder", + "boardNotWritable": "Non hai i permessi di scrittura per la bacheca \"{{boardName}}\". Seleziona una bacheca di tua proprietà oppure passa a Non categorizzata.", + "noFlux2KleinVaeModelSelected": "Nessun VAE selezionato. I modelli FLUX.2 Klein senza diffusori richiedono un VAE autonomo", + "noFlux2KleinQwen3EncoderModelSelected": "Nessun encoder Qwen3 selezionato. I modelli Klein FLUX.2 senza diffusori richiedono un encoder Qwen3 autonomo" + }, + "useCpuNoise": "Usa la CPU per generare rumore", + "iterations": "Iterazioni", + "imageActions": "Azioni Immagine", + "cfgRescaleMultiplier": "Moltiplicatore riscala CFG", + "useSize": "Usa Dimensioni", + "setToOptimalSize": "Ottimizza le dimensioni per il modello", + "setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (potrebbe essere troppo piccolo)", + "lockAspectRatio": "Blocca proporzioni", + "swapDimensions": "Scambia dimensioni", + "aspect": "Aspetto", + "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (potrebbe essere troppo grande)", + "remixImage": "Remixa l'immagine", + "coherenceEdgeSize": "Dim. bordo", + "infillColorValue": "Colore di riempimento", + "processImage": "Elabora Immagine", + "sendToUpscale": "Invia a Amplia", + "postProcessing": "Post-elaborazione (Shift + U)", + "guidance": "Guida", + "gaussianBlur": "Sfocatura Gaussiana", + "boxBlur": "Sfocatura Box", + "staged": "Maschera espansa", + "optimizedImageToImage": "Immagine-a-immagine ottimizzata", + "sendToCanvas": "Invia alla Tela", + "coherenceMinDenoise": "Min rid. rumore", + "recallMetadata": "Richiama i metadati", + "disabledNoRasterContent": "Disabilitato (nessun contenuto Raster)", + "modelDisabledForTrial": "La generazione con {{modelName}} non è disponibile per gli account di prova. Visita le impostazioni account per effettuare l'upgrade.", + "useClipSkip": "Usa CLIP Skip", + "duration": "Durata", + "images_withCount_one": "Immagine", + "images_withCount_many": "Immagini", + "images_withCount_other": "Immagini", + "resolution": "Risoluzione", + "downloadImage": "Scarica l'immagine", + "showOptionsPanel": "Mostra pannello laterale (O o T)", + "seedVarianceRandomizePercent": "Percentuale di variazione", + "seedVarianceStrength": "Intensità della varianza", + "seedVarianceEnabled": "Migliora varianza seme", + "colorCompensation": "Compensazione Colore", + "disabledNotSupported": "Non supportato dal modello", + "imageSize": "Dimensioni immagine" + }, + "settings": { + "models": "Modelli", + "displayInProgress": "Visualizza le immagini di avanzamento", + "confirmOnDelete": "Conferma l'eliminazione", + "resetWebUI": "Reimposta l'interfaccia utente Web", + "resetWebUIDesc1": "Il ripristino dell'interfaccia utente Web reimposta solo la cache locale del browser delle immagini e le impostazioni memorizzate. Non cancella alcuna immagine dal disco.", + "resetWebUIDesc2": "Se le immagini non vengono visualizzate nella galleria o qualcos'altro non funziona, prova a reimpostare prima di segnalare un problema su GitHub.", + "resetComplete": "L'interfaccia utente Web è stata reimpostata.", + "general": "Generale", + "developer": "Sviluppatore", + "antialiasProgressImages": "Anti aliasing delle immagini di avanzamento", + "showProgressInViewer": "Mostra le immagini di avanzamento nel visualizzatore", + "generation": "Generazione", + "ui": "Interfaccia Utente", + "beta": "Beta", + "clearIntermediates": "Cancella le immagini intermedie", + "clearIntermediatesDesc3": "Le immagini della galleria non verranno eliminate.", + "clearIntermediatesDesc2": "Le immagini intermedie sono sottoprodotti della generazione, diversi dalle immagini risultanti nella galleria. La cancellazione degli intermedi libererà spazio su disco.", + "intermediatesCleared_one": "Cancellata {{count}} immagine intermedia", + "intermediatesCleared_many": "Cancellate {{count}} immagini intermedie", + "intermediatesCleared_other": "Cancellate {{count}} immagini intermedie", + "clearIntermediatesDesc1": "La cancellazione delle immagini intermedie ripristinerà lo stato della Tela e degli Adattatori di Controllo.", + "intermediatesClearedFailed": "Problema con la cancellazione delle immagini intermedie", + "clearIntermediatesWithCount_one": "Cancella {{count}} immagine intermedia", + "clearIntermediatesWithCount_many": "Cancella {{count}} immagini intermedie", + "clearIntermediatesWithCount_other": "Cancella {{count}} immagini intermedie", + "clearIntermediatesDisabled": "La coda deve essere vuota per cancellare le immagini intermedie", + "enableNSFWChecker": "Abilita controllo NSFW", + "enableInvisibleWatermark": "Abilita filigrana invisibile", + "enableInformationalPopovers": "Abilita testo informativo a comparsa", + "reloadingIn": "Ricaricando in", + "informationalPopoversDisabled": "Testo informativo a comparsa disabilitato", + "informationalPopoversDisabledDesc": "I testi informativi a comparsa sono disabilitati. Attivali nelle impostazioni.", + "confirmOnNewSession": "Conferma su nuova sessione", + "enableModelDescriptions": "Abilita le descrizioni dei modelli nei menu a discesa", + "showDetailedInvocationProgress": "Mostra dettagli avanzamento", + "enableHighlightFocusedRegions": "Evidenzia le regioni interessate", + "modelDescriptionsDisabled": "Descrizioni dei modelli nei menu a discesa disabilitate", + "modelDescriptionsDisabledDesc": "Le descrizioni dei modelli nei menu a discesa sono state disattivate. Abilitale nelle Impostazioni.", + "preferAttentionStyleNumeric": "Preferisci lo stile di attenzione numerico", + "maxQueueHistory": "Cronologia massima della coda", + "maxQueueHistorySaveFailed": "Impossibile salvare la cronologia della coda massima", + "middleClickOpenInNewTab": "Utilizza il clic centrale del mouse per aprire le immagini in una nuova scheda", + "externalProviders": "Fornitori esterni", + "externalProviderConfigured": "Configurato", + "externalProviderNotConfigured": "Chiave API necessaria", + "externalProviderNotConfiguredHint": "Aggiungi la tua chiave API in Gestione Modello o nella configurazione del server per abilitare questo provider.", + "imageSubfolderStrategy": "Strategia per le sottocartelle delle immagini", + "imageSubfolderStrategyDate": "Data", + "imageSubfolderStrategySaveFailed": "Impossibile salvare la strategia della sottocartella Immagine", + "imageSubfolderStrategyType": "Tipo", + "imageSubfolderStrategyUnknown": "({{strategy}}) sconosciuta" + }, + "toast": { + "uploadFailed": "Caricamento fallito", + "imageCopied": "Immagine copiata", + "parametersNotSet": "Parametri non richiamati", + "serverError": "Errore del Server", + "connected": "Connesso al server", + "canceled": "Elaborazione annullata", + "uploadFailedInvalidUploadDesc": "Devono essere immagini PNG, JPEG o WEBP.", + "parameterSet": "Parametro richiamato", + "parameterNotSet": "Parametro non richiamato", + "problemCopyingImage": "Impossibile copiare l'immagine", + "baseModelChangedCleared_one": "Aggiornato, cancellato o disabilitato {{count}} sottomodello incompatibile", + "baseModelChangedCleared_many": "Aggiornati, cancellati o disabilitati {{count}} sottomodelli incompatibili", + "baseModelChangedCleared_other": "Cancellati o disabilitati {{count}} sottomodelli incompatibili", + "loadedWithWarnings": "Flusso di lavoro caricato con avvisi", + "imageUploaded": "Immagine caricata", + "addedToBoard": "Aggiunto alle risorse della bacheca {{name}}", + "modelAddedSimple": "Modello aggiunto alla Coda", + "imageUploadFailed": "Caricamento immagine non riuscito", + "workflowLoaded": "Flusso di lavoro caricato", + "problemDeletingWorkflow": "Problema durante l'eliminazione del flusso di lavoro", + "workflowDeleted": "Flusso di lavoro eliminato", + "problemRetrievingWorkflow": "Problema nel recupero del flusso di lavoro", + "problemDownloadingImage": "Impossibile scaricare l'immagine", + "prunedQueue": "Coda ripulita", + "modelImportCanceled": "Importazione del modello annullata", + "parameters": "Parametri", + "parameterSetDesc": "{{parameter}} richiamato", + "parameterNotSetDesc": "Impossibile richiamare {{parameter}}", + "parameterNotSetDescWithMessage": "Impossibile richiamare {{parameter}}: {{message}}", + "parametersSet": "Parametri richiamati", + "errorCopied": "Errore copiato", + "outOfMemoryError": "Errore di memoria esaurita", + "baseModelChanged": "Modello base modificato", + "sessionRef": "Sessione: {{sessionId}}", + "somethingWentWrong": "Qualcosa è andato storto", + "outOfMemoryErrorDesc": "Le impostazioni della generazione attuale superano la capacità del sistema. Modifica le impostazioni e riprova.", + "importFailed": "Importazione non riuscita", + "importSuccessful": "Importazione riuscita", + "problemSavingLayer": "Impossibile salvare il livello", + "unableToLoadImage": "Impossibile caricare l'immagine", + "problemCopyingLayer": "Impossibile copiare il livello", + "sentToCanvas": "Inviato alla Tela", + "sentToUpscale": "Inviato a Amplia", + "unableToLoadStylePreset": "Impossibile caricare lo stile predefinito", + "stylePresetLoaded": "Stile predefinito caricato", + "unableToLoadImageMetadata": "Impossibile caricare i metadati dell'immagine", + "layerCopiedToClipboard": "Livello copiato negli appunti", + "linkCopied": "Collegamento copiato", + "addedToUncategorized": "Aggiunto alle risorse della bacheca $t(boards.uncategorized)", + "imagesWillBeAddedTo": "Le immagini caricate verranno aggiunte alle risorse della bacheca {{boardName}}.", + "outOfMemoryErrorDescLocal": "Segui la nostra guida per bassa VRAM per ridurre gli OOM.", + "pasteFailed": "Incolla non riuscita", + "pasteSuccess": "Incollato su {{destination}}", + "unableToCopy": "Impossibile copiare", + "unableToCopyDesc": "Il tuo browser non supporta l'accesso agli appunti. Gli utenti di Firefox potrebbero risolvere il problema seguendo ", + "unableToCopyDesc_theseSteps": "questi passaggi", + "fluxFillIncompatibleWithT2IAndI2I": "FLUX Fill non è compatibile con Testo a Immagine o Immagine a Immagine. Per queste attività, utilizzare altri modelli FLUX.", + "problemUnpublishingWorkflow": "Problema durante l'annullamento della pubblicazione del flusso di lavoro", + "problemUnpublishingWorkflowDescription": "Si è verificato un problema durante l'annullamento della pubblicazione del flusso di lavoro. Riprova.", + "workflowUnpublished": "Flusso di lavoro non pubblicato", + "chatGPT4oIncompatibleGenerationMode": "ChatGPT 4o supporta solo la conversione da testo a immagine e da immagine a immagine. Utilizza altri modelli per le attività di Inpainting e Outpainting.", + "imagenIncompatibleGenerationMode": "Google {{model}} supporta solo la generazione da testo a immagine. Utilizza altri modelli per le attività di conversione da immagine a immagine, inpainting e outpainting.", + "noVisibleRasterLayers": "Nessun livello raster visibile", + "noVisibleRasterLayersDesc": "Abilitare almeno un livello raster da esportare in PSD", + "invalidCanvasDimensions": "Dimensioni della tela non valide", + "canvasTooLarge": "Tela troppo grande", + "canvasTooLargeDesc": "Le dimensioni della tela superano le dimensioni massime consentite per l'esportazione in formato PSD. Riduci la larghezza e l'altezza totali della tela e riprova.", + "psdExportSuccess": "Esportazione PSD completata", + "psdExportSuccessDesc": "Esportazione riuscita di {{count}} livelli nel file PSD", + "problemExportingPSD": "Problema durante l'esportazione PSD", + "fluxKontextIncompatibleGenerationMode": "FLUX Kontext non supporta la generazione di immagini posizionate sulla tela. Riprova utilizzando la sezione Immagine di riferimento e disattiva tutti i livelli raster.", + "canvasManagerNotAvailable": "Gestione tela non disponibile", + "promptExpansionFailed": "Abbiamo riscontrato un problema. Riprova a eseguire l'espansione del prompt.", + "uploadAndPromptGenerationFailed": "Impossibile caricare l'immagine e generare il prompt", + "promptGenerationStarted": "Generazione del prompt avviata", + "noVisibleMasksDesc": "Crea o abilita almeno una maschera inpaint da invertire", + "noVisibleMasks": "Nessuna maschera visibile", + "maskInvertFailed": "Impossibile invertire la maschera", + "maskInverted": "Maschera invertita", + "uploadFailedInvalidUploadDesc_withCount_one": "Deve essere presente al massimo 1 immagine PNG, JPEG o WEBP.", + "uploadFailedInvalidUploadDesc_withCount_many": "Devono essere presenti al massimo {{count}} immagini PNG, JPEG o WEBP.", + "uploadFailedInvalidUploadDesc_withCount_other": "Devono essere presenti al massimo {{count}} immagini PNG, JPEG o WEBP.", + "imageNotLoadedDesc": "Impossibile trovare l'immagine", + "imageSaved": "Immagine salvata", + "imageSavingFailed": "Salvataggio dell'immagine non riuscito", + "invalidUpload": "Caricamento non valido", + "layerSavedToAssets": "Livello salvato nelle risorse", + "noRasterLayers": "Nessun livello raster trovato", + "noRasterLayersDesc": "Crea almeno un livello raster da esportare in PSD", + "noActiveRasterLayers": "Nessun livello raster attivo", + "noActiveRasterLayersDesc": "Abilita almeno un livello raster da esportare in PSD", + "failedToProcessLayers": "Impossibile elaborare i livelli", + "noValidLayerAdapters": "Nessun adattatore di livello valido trovato", + "setControlImage": "Imposta come immagine di controllo", + "setNodeField": "Imposta come campo nodo", + "noInpaintMaskSelected": "Nessuna maschera di inpaint selezionata", + "noInpaintMaskSelectedDesc": "Seleziona una maschera di inpaint da invertire", + "invalidBbox": "Riquadro di delimitazione non valido", + "invalidBboxDesc": "Il riquadro di delimitazione non ha dimensioni valide", + "kleinEncoderClearedDescription": "Selezionare un encoder Qwen3 compatibile per la nuova variante del modello Klein", + "kleinEncoderCleared": "Encoder Qwen3 cancellato", + "schedulerReset": "Ripristino campionatore", + "schedulerResetZImageBase": "Il campionatore LCM non è compatibile con i modelli Z-Image Base. Reimpostare su Euler.", + "modelDownloadPaused": "Download del modello in pausa", + "modelDownloadResumed": "Ripresa del download", + "modelDownloadRestartFailed": "Riavvia i download non riusciti", + "modelDownloadRestartFile": "Riavvio del download del file", + "modelDownloadRestartedFromScratch": "Manca una parte del file. Riavviato il download dall'inizio." + }, + "accessibility": { + "invokeProgressBar": "Barra di avanzamento generazione", + "uploadImage": "Carica immagine", + "previousImage": "Immagine precedente", + "nextImage": "Immagine successiva", + "reset": "Reimposta", + "menu": "Menu", + "mode": "Modalità", + "resetUI": "$t(accessibility.reset) l'Interfaccia Utente", + "createIssue": "Segnala un problema", + "about": "Informazioni", + "submitSupportTicket": "Invia ticket di supporto", + "toggleLeftPanel": "Attiva/disattiva il pannello sinistro (T)", + "toggleRightPanel": "Attiva/disattiva il pannello destro (G)", + "uploadImages": "Carica immagine(i)" + }, + "nodes": { + "zoomOutNodes": "Rimpicciolire", + "hideMinimapnodes": "Nascondi minimappa", + "showMinimapnodes": "Mostra minimappa", + "zoomInNodes": "Ingrandire", + "fitViewportNodes": "Adatta vista", + "reloadNodeTemplates": "Ricarica i modelli di nodo", + "loadWorkflow": "Importa flusso di lavoro", + "downloadWorkflow": "Esporta flusso di lavoro JSON", + "scheduler": "Campionatore", + "addNode": "Aggiungi nodo", + "animatedEdgesHelp": "Anima i bordi selezionati e i bordi collegati ai nodi selezionati", + "executionStateInProgress": "In corso", + "executionStateError": "Errore", + "executionStateCompleted": "Completato", + "addNodeToolTip": "Aggiungi nodo (Shift+A, Space)", + "colorCodeEdgesHelp": "Bordi con codice colore in base ai campi collegati", + "animatedEdges": "Bordi animati", + "snapToGrid": "Aggancia alla griglia", + "validateConnections": "Convalida connessioni e grafico", + "validateConnectionsHelp": "Impedisce che vengano effettuate connessioni non valide e che vengano \"invocati\" grafici non validi", + "fullyContainNodesHelp": "I nodi devono essere completamente all'interno della casella di selezione per essere selezionati", + "fullyContainNodes": "Contenere completamente i nodi da selezionare", + "snapToGridHelp": "Aggancia i nodi alla griglia quando vengono spostati", + "workflowSettings": "Impostazioni Editor del flusso di lavoro", + "colorCodeEdges": "Bordi con codice colore", + "noOutputRecorded": "Nessun output registrato", + "workflowDescription": "Breve descrizione", + "workflowContact": "Contatto", + "workflowVersion": "Versione", + "workflow": "Flusso di lavoro", + "noWorkflow": "Nessun flusso di lavoro", + "workflowTags": "Etichette", + "workflowValidation": "Errore di convalida del flusso di lavoro", + "workflowAuthor": "Autore", + "workflowName": "Nome", + "workflowNotes": "Note", + "unableToValidateWorkflow": "Impossibile convalidare il flusso di lavoro", + "updateApp": "Aggiorna Applicazione", + "updateNode": "Aggiorna nodo", + "version": "Versione", + "notes": "Note", + "problemSettingTitle": "Problema nell'impostazione del titolo", + "nodeType": "Tipo di nodo", + "notesDescription": "Aggiunge note sul tuo flusso di lavoro", + "unknownField": "Campo sconosciuto", + "unknownNode": "Nodo sconosciuto", + "missingTemplate": "Nodo non valido: nodo {{node}} di tipo {{type}} modello mancante (non installato?)", + "noNodeSelected": "Nessun nodo selezionato", + "nodeTemplate": "Modello di nodo", + "nodeOpacity": "Opacità del nodo", + "nodeSearch": "Cerca nodi", + "nodeOutputs": "Uscite del nodo", + "noConnectionInProgress": "Nessuna connessione in corso", + "cannotDuplicateConnection": "Impossibile creare connessioni duplicate", + "boolean": "Booleani", + "node": "Nodo", + "collection": "Raccolta", + "cannotConnectInputToInput": "Impossibile collegare ingresso a ingresso", + "cannotConnectOutputToOutput": "Impossibile collegare uscita ad uscita", + "cannotConnectToSelf": "Impossibile connettersi a se stesso", + "loadingNodes": "Caricamento nodi...", + "enum": "Enumeratore", + "float": "Decimale", + "currentImageDescription": "Visualizza l'immagine corrente nell'editor dei nodi", + "fieldTypesMustMatch": "I tipi di campo devono corrispondere", + "edge": "Collegamento", + "currentImage": "Immagine corrente", + "integer": "Numero Intero", + "inputMayOnlyHaveOneConnection": "L'ingresso può avere solo una connessione", + "ipAdapter": "Adattatore IP", + "string": "Stringa", + "connectionWouldCreateCycle": "La connessione creerebbe un ciclo", + "updateAllNodes": "Aggiorna i nodi", + "unableToUpdateNodes_one": "Impossibile aggiornare {{count}} nodo", + "unableToUpdateNodes_many": "Impossibile aggiornare {{count}} nodi", + "unableToUpdateNodes_other": "Impossibile aggiornare {{count}} nodi", + "unknownErrorValidatingWorkflow": "Errore sconosciuto durante la convalida del flusso di lavoro", + "collectionFieldType": "{{name}} (Raccolta)", + "collectionOrScalarFieldType": "{{name}} (Singola o Raccolta)", + "nodeVersion": "Versione Nodo", + "inputFieldTypeParseError": "Impossibile analizzare il tipo di campo di input {{node}}.{{field}} ({{message}})", + "unsupportedArrayItemType": "Tipo di elemento dell'array non supportato \"{{type}}\"", + "targetNodeFieldDoesNotExist": "Connessione non valida: il campo di destinazione/input {{node}}.{{field}} non esiste", + "unsupportedMismatchedUnion": "tipo CollectionOrScalar non corrispondente con tipi di base {{firstType}} e {{secondType}}", + "allNodesUpdated": "Tutti i nodi sono aggiornati", + "sourceNodeDoesNotExist": "Connessione non valida: il nodo di origine/output {{node}} non esiste", + "unableToExtractEnumOptions": "Impossibile estrarre le opzioni enum", + "unableToParseFieldType": "Impossibile analizzare il tipo di campo", + "outputFieldTypeParseError": "Impossibile analizzare il tipo di campo di output {{node}}.{{field}} ({{message}})", + "sourceNodeFieldDoesNotExist": "Connessione non valida: il campo di origine/output {{node}}.{{field}} non esiste", + "unableToGetWorkflowVersion": "Impossibile ottenere la versione dello schema del flusso di lavoro", + "nodePack": "Pacchetto di nodi", + "unableToExtractSchemaNameFromRef": "Impossibile estrarre il nome dello schema dal riferimento", + "unknownNodeType": "Tipo di nodo sconosciuto", + "targetNodeDoesNotExist": "Connessione non valida: il nodo di destinazione/input {{node}} non esiste", + "unknownFieldType": "$t(nodes.unknownField) tipo: {{type}}", + "deletedInvalidEdge": "Eliminata connessione non valida {{source}} -> {{target}}", + "prototypeDesc": "Questa invocazione è un prototipo. Potrebbe subire modifiche sostanziali durante gli aggiornamenti dell'app e potrebbe essere rimossa in qualsiasi momento.", + "betaDesc": "Questa invocazione è in versione beta. Fino a quando non sarà stabile, potrebbe subire modifiche importanti durante gli aggiornamenti dell'app. Abbiamo intenzione di supportare questa invocazione a lungo termine.", + "newWorkflow": "Nuovo flusso di lavoro", + "newWorkflowDesc": "Creare un nuovo flusso di lavoro?", + "newWorkflowDesc2": "Il flusso di lavoro attuale presenta modifiche non salvate.", + "unsupportedAnyOfLength": "unione di troppi elementi ({{count}})", + "clearWorkflowDesc": "Cancellare questo flusso di lavoro e avviarne uno nuovo?", + "clearWorkflow": "Cancella il flusso di lavoro", + "clearWorkflowDesc2": "Il tuo flusso di lavoro attuale presenta modifiche non salvate.", + "viewMode": "Usa la vista lineare", + "editMode": "Modifica nell'editor del flusso di lavoro", + "resetToDefaultValue": "Ripristina il valore predefinito", + "noFieldsViewMode": "Questo flusso di lavoro non ha campi selezionati da visualizzare. Visualizza il flusso di lavoro completo per configurare i valori.", + "edit": "Modifica", + "graph": "Grafico", + "showEdgeLabelsHelp": "Mostra etichette sui collegamenti, che indicano i nodi collegati", + "showEdgeLabels": "Mostra le etichette del collegamento", + "cannotMixAndMatchCollectionItemTypes": "Impossibile combinare e abbinare i tipi di elementi della raccolta", + "noGraph": "Nessun grafico", + "missingNode": "Nodo di invocazione mancante", + "missingInvocationTemplate": "Modello di invocazione mancante", + "missingFieldTemplate": "Modello di campo mancante", + "singleFieldType": "{{name}} (Singola)", + "imageAccessError": "Impossibile trovare l'immagine {{image_name}}, ripristino ai valori predefiniti", + "boardAccessError": "Impossibile trovare la bacheca {{board_id}}, ripristino ai valori predefiniti", + "modelAccessError": "Impossibile trovare il modello {{key}}, ripristino ai valori predefiniti", + "saveToGallery": "Salva nella Galleria", + "noMatchingWorkflows": "Nessun flusso di lavoro corrispondente", + "noWorkflows": "Nessun flusso di lavoro", + "workflowHelpText": "Hai bisogno di aiuto? Consulta la nostra guida Introduzione ai flussi di lavoro.", + "specialDesc": "Questa invocazione comporta una gestione speciale nell'applicazione. Ad esempio, i nodi Lotto vengono utilizzati per mettere in coda più grafici da un singolo flusso di lavoro.", + "internalDesc": "Questa invocazione è utilizzata internamente da Invoke. Potrebbe subire modifiche significative durante gli aggiornamenti dell'app e potrebbe essere rimossa in qualsiasi momento.", + "addItem": "Aggiungi elemento", + "generatorNoValues": "vuoto", + "linearDistribution": "Distribuzione lineare", + "parseString": "Analizza stringa", + "splitOn": "Diviso su", + "noBatchGroup": "nessun gruppo", + "generatorLoadFromFile": "Carica da file", + "dynamicPromptsRandom": "Prompt dinamici (casuali)", + "dynamicPromptsCombinatorial": "Prompt dinamici (combinatori)", + "uniformRandomDistribution": "Distribuzione casuale uniforme", + "generatorNRandomValues_one": "{{count}} valore casuale", + "generatorNRandomValues_many": "{{count}} valori casuali", + "generatorNRandomValues_other": "{{count}} valori casuali", + "arithmeticSequence": "Sequenza aritmetica", + "nodeName": "Nome del nodo", + "loadWorkflowDesc": "Caricare il flusso di lavoro?", + "loadWorkflowDesc2": "Il flusso di lavoro corrente presenta modifiche non salvate.", + "downloadWorkflowError": "Errore durante lo scaricamento del flusso di lavoro", + "deletedMissingNodeFieldFormElement": "Campo modulo mancante eliminato: nodo {{nodeId}} campo {{fieldName}}", + "unableToUpdateNode": "Aggiornamento del nodo non riuscito: nodo {{node}} di tipo {{type}} (potrebbe essere necessario eliminarlo e ricrearlo)", + "description": "Descrizione", + "generatorImagesCategory": "Categoria", + "generatorImages_one": "{{count}} immagine", + "generatorImages_many": "{{count}} immagini", + "generatorImages_other": "{{count}} immagini", + "generatorImagesFromBoard": "Immagini dalla Bacheca", + "missingSourceOrTargetNode": "Nodo sorgente o di destinazione mancante", + "unknownField_withName": "Campo \"{{name}}\" sconosciuto", + "missingField_withName": "Campo \"{{name}}\" mancante", + "unknownFieldEditWorkflowToFix_withName": "Il flusso di lavoro contiene un campo \"{{name}}\" sconosciuto .\nModifica il flusso di lavoro per risolvere il problema.", + "unexpectedField_withName": "Campo \"{{name}}\" inaspettato", + "missingSourceOrTargetHandle": "Identificatore del nodo sorgente o di destinazione mancante", + "layout": { + "alignmentDR": "In basso a destra", + "autoLayout": "Schema automatico", + "nodeSpacing": "Spaziatura nodi", + "layerSpacing": "Spaziatura livelli", + "layeringStrategy": "Strategia livelli", + "longestPath": "Percorso più lungo", + "layoutDirection": "Direzione schema", + "layoutDirectionRight": "A destra", + "layoutDirectionDown": "In basso", + "alignment": "Allineamento nodi", + "alignmentUL": "In alto a sinistra", + "alignmentDL": "In basso a sinistra", + "alignmentUR": "In alto a destra" + }, + "generatorLoading": "caricamento", + "addLinearView": "Aggiungi alla vista lineare", + "hideLegendNodes": "Nascondi legenda tipo di campo", + "mismatchedVersion": "Nodo non valido: il nodo {{node}} di tipo {{type}} ha una versione non corrispondente (provare ad aggiornare?)", + "noFieldsLinearview": "Nessun campo aggiunto alla vista lineare", + "removeLinearView": "Rimuovi dalla vista lineare", + "reorderLinearView": "Riordina vista lineare", + "showLegendNodes": "Mostra legenda tipo di campo", + "unableToLoadWorkflow": "Impossibile caricare il flusso di lavoro", + "unknownTemplate": "Modello sconosciuto", + "unknownInput": "Input sconosciuto: {{name}}", + "loadingTemplates": "Caricamento in corso {{name}}", + "versionUnknown": " Versione sconosciuta", + "generateValues": "Genera valori", + "floatRangeGenerator": "Generatore di intervallo di numeri decimali", + "integerRangeGenerator": "Generatore di intervallo di numeri interi", + "noWorkflowToSave": "Nessun flusso di lavoro da salvare", + "nodeData": "Dati del nodo", + "groupNodesByCategory": "Raggruppa i nodi per categoria", + "groupNodesByCategoryHelp": "Raggruppa i nodi per categoria nella finestra di dialogo \"Aggiungi nodo\"", + "addConnector": "Aggiungi connettore", + "deleteConnector": "Elimina connettore" + }, + "boards": { + "autoAddBoard": "Aggiungi automaticamente bacheca", + "menuItemAutoAdd": "Aggiungi automaticamente a questa bacheca", + "cancel": "Annulla", + "addBoard": "Aggiungi Bacheca", + "bottomMessage": "L'eliminazione delle immagini reimposterà tutte le funzionalità che le stanno utilizzando.", + "changeBoard": "Cambia Bacheca", + "loading": "Caricamento in corso ...", + "clearSearch": "Cancella Ricerca", + "topMessage": "Questa selezione contiene immagini utilizzate nelle seguenti funzionalità:", + "move": "Sposta", + "myBoard": "Bacheca", + "searchBoard": "Cerca bacheche ...", + "noMatching": "Nessuna bacheca corrispondente", + "selectBoard": "Seleziona una bacheca", + "uncategorized": "Non categorizzato", + "downloadBoard": "Scarica la bacheca", + "deleteBoardOnly": "solo la Bacheca", + "deleteBoard": "Elimina Bacheca", + "deleteBoardAndImages": "Bacheca e Immagini", + "deletedBoardsCannotbeRestored": "Le bacheche e le immagini eliminate non possono essere ripristinate. Selezionando \"Elimina solo bacheca\" le immagini verranno spostate in uno stato non categorizzato.", + "movingImagesToBoard_one": "Spostare {{count}} immagine nella bacheca:", + "movingImagesToBoard_many": "Spostare {{count}} immagini nella bacheca:", + "movingImagesToBoard_other": "Spostare {{count}} immagini nella bacheca:", + "imagesWithCount_one": "{{count}} immagine", + "imagesWithCount_many": "{{count}} immagini", + "imagesWithCount_other": "{{count}} immagini", + "assetsWithCount_one": "{{count}} risorsa", + "assetsWithCount_many": "{{count}} risorse", + "assetsWithCount_other": "{{count}} risorse", + "archiveBoard": "Archivia la bacheca", + "archived": "Archiviato", + "unarchiveBoard": "Annulla l'archiviazione della bacheca", + "selectedForAutoAdd": "Selezionato per l'aggiunta automatica", + "addSharedBoard": "Aggiungi una Bacheca Condivisa", + "boards": "Bacheche", + "private": "Bacheche private", + "shared": "Bacheche condivise", + "addPrivateBoard": "Aggiungi una Bacheca Privata", + "noBoards": "Nessuna bacheca {{boardType}}", + "deletedPrivateBoardsCannotbeRestored": "Le bacheche e le immagini eliminate non possono essere ripristinate. Selezionando \"Elimina solo bacheca\", le immagini verranno spostate in uno stato privato e non categorizzato per l'autore dell'immagine.", + "updateBoardError": "Errore durante l'aggiornamento della bacheca", + "uncategorizedImages": "Immagini non categorizzate", + "deleteAllUncategorizedImages": "Elimina tutte le immagini non categorizzate", + "locateInGalery": "Trova nella Galleria", + "deletedImagesCannotBeRestored": "Le immagini eliminate non possono essere ripristinate.", + "hideBoards": "Nascondi bacheche", + "viewBoards": "Visualizza le bacheche", + "pause": "Pausa", + "resume": "Riprendi", + "restartFailed": "Riavvio non riuscito", + "restartFile": "Riavvia il file", + "restartRequired": "Riavvio richiesto", + "resumeRefused": "Ripristino rifiutato dal server. Riavvio richiesto.", + "setBoardVisibility": "Visibilità della bacheca", + "setVisibilityPrivate": "Imposta come privata", + "setVisibilityShared": "Imposta come condivisa", + "setVisibilityPublic": "Imposta come pubblica", + "visibilityPrivate": "Privata", + "visibilityShared": "Condivisa", + "visibilityPublic": "Pubblica", + "visibilityBadgeShared": "Bacheca condivisa", + "visibilityBadgePublic": "Bacheca pubblica", + "updateBoardVisibilityError": "Errore durante l'aggiornamento della visibilità della bacheca" + }, + "queue": { + "queueFront": "Aggiungi all'inizio della coda", + "queueBack": "Aggiungi alla coda", + "queue": "Coda", + "status": "Stato", + "pruneSucceeded": "Rimossi {{item_count}} elementi completati dalla coda", + "cancelTooltip": "Annulla l'elemento corrente", + "queueEmpty": "Coda vuota", + "pauseSucceeded": "Elaborazione sospesa", + "in_progress": "In corso", + "notReady": "Impossibile mettere in coda", + "batchFailedToQueue": "Impossibile mettere in coda il lotto", + "completed": "Completati", + "cancelFailed": "Problema durante l'annullamento dell'elemento", + "batchQueued": "Lotto aggiunto alla coda", + "pauseFailed": "Problema durante la sospensione dell'elaborazione", + "clearFailed": "Problema nella cancellazione della coda", + "front": "inizio", + "clearSucceeded": "Coda cancellata", + "pause": "Sospendi", + "pruneTooltip": "Rimuovi {{item_count}} elementi completati", + "cancelSucceeded": "Elemento annullato", + "batchQueuedDesc_one": "Aggiunta {{count}} sessione a {{direction}} della coda", + "batchQueuedDesc_many": "Aggiunte {{count}} sessioni a {{direction}} della coda", + "batchQueuedDesc_other": "Aggiunte {{count}} sessioni a {{direction}} della coda", + "graphQueued": "Grafico in coda", + "batch": "Lotto", + "clearQueueAlertDialog": "La cancellazione della coda annulla immediatamente tutti gli elementi in elaborazione e cancella completamente la coda. I filtri in sospeso verranno annullati e l'area di lavoro della Tela verrà reimpostata.", + "pending": "In attesa", + "completedIn": "Completato in", + "resumeFailed": "Problema nel riavvio dell'elaborazione", + "clear": "Cancella", + "prune": "Rimuovi", + "total": "Totale", + "canceled": "Annullati", + "pruneFailed": "Problema nel rimuovere la coda", + "cancelBatchSucceeded": "Lotto annullato", + "clearTooltip": "Annulla e cancella tutti gli elementi", + "current": "Attuale", + "pauseTooltip": "Sospendi l'elaborazione", + "failed": "Falliti", + "cancelItem": "Annulla l'elemento", + "next": "Prossimo", + "cancelBatch": "Annulla lotto", + "back": "fine", + "cancel": "Annulla", + "session": "Sessione", + "resumeSucceeded": "Elaborazione ripresa", + "enqueueing": "Lotto in coda", + "resumeTooltip": "Riprendi l'elaborazione", + "resume": "Riprendi", + "cancelBatchFailed": "Problema durante l'annullamento del lotto", + "clearQueueAlertDialog2": "Sei sicuro di voler cancellare la coda?", + "item": "Elemento", + "graphFailedToQueue": "Impossibile mettere in coda il grafico", + "time": "Tempo", + "openQueue": "Apri coda", + "iterations_one": "Iterazione", + "iterations_many": "Iterazioni", + "iterations_other": "Iterazioni", + "prompts_one": "Prompt", + "prompts_many": "Prompt", + "prompts_other": "Prompt", + "generations_one": "Generazione", + "generations_many": "Generazioni", + "generations_other": "Generazioni", + "origin": "Origine", + "destination": "Dest", + "upscaling": "Ampliamento", + "canvas": "Tela", + "workflows": "Flussi di lavoro", + "generation": "Generazione", + "other": "Altro", + "gallery": "Galleria", + "batchSize": "Dimensione del lotto", + "cancelAllExceptCurrentQueueItemAlertDialog2": "Vuoi davvero annullare tutti gli elementi in coda in sospeso?", + "confirm": "Conferma", + "cancelAllExceptCurrentQueueItemAlertDialog": "L'annullamento di tutti gli elementi della coda, eccetto quello corrente, interromperà gli elementi in sospeso ma consentirà il completamento di quello in corso.", + "cancelAllExceptCurrentTooltip": "Annulla tutto tranne l'elemento corrente", + "retrySucceeded": "Elemento rieseguito", + "retryItem": "Riesegui elemento", + "retryFailed": "Problema riesecuzione elemento", + "credits": "Crediti", + "cancelAllExceptCurrent": "Annulla tutto tranne quello corrente", + "sortColumn": "Ordina colonna", + "sortBy": "Ordina per {{column}}", + "sortOrderAscending": "Ascendente", + "sortOrderDescending": "Discendente", + "createdAt": "Creato", + "completedAt": "Completato", + "batchFieldValues": "Valori del campo Lotto", + "paused": "In pausa", + "cancelFailedAccessDenied": "Problema durante l'annullamento dell'articolo: accesso negato", + "clearFailedAccessDenied": "Problema durante la cancellazione della coda: accesso negato", + "user": "Utente", + "cannotViewDetails": "Non hai l'autorizzazione per visualizzare i dettagli di questo elemento della coda", + "fieldValuesHidden": "", + "queueActionsMenu": "Menu azioni in coda", + "queueItem": "Elemento della coda" + }, + "models": { + "noMatchingModels": "Nessun modello corrispondente", + "loading": "caricamento", + "noModelsAvailable": "Nessun modello disponibile", + "selectModel": "Seleziona un modello", + "noRefinerModelsInstalled": "Nessun modello affinatore SDXL installato", + "addLora": "Aggiungi LoRA", + "defaultVAE": "VAE predefinito", + "concepts": "Concetti", + "lora": "LoRA", + "noCompatibleLoRAs": "Nessun LoRA compatibile", + "noMatchingLoRAs": "Nessun LoRA corrispondente", + "noLoRAsInstalled": "Nessun LoRA installato" + }, + "invocationCache": { + "disable": "Disabilita", + "misses": "Non trovati in cache", + "enableFailed": "Problema nell'abilitazione della cache delle invocazioni", + "invocationCache": "Cache delle invocazioni", + "clearSucceeded": "Cache delle invocazioni svuotata", + "enableSucceeded": "Cache delle invocazioni abilitata", + "clearFailed": "Problema durante lo svuotamento della cache delle invocazioni", + "hits": "Trovati in cache", + "disableSucceeded": "Cache delle invocazioni disabilitata", + "disableFailed": "Problema durante la disabilitazione della cache delle invocazioni", + "enable": "Abilita", + "clear": "Svuota", + "maxCacheSize": "Dimensione max cache", + "cacheSize": "Dimensione cache", + "useCache": "Usa Cache" + }, + "dynamicPrompts": { + "seedBehaviour": { + "perPromptDesc": "Utilizza un seme diverso per ogni immagine", + "perIterationLabel": "Per iterazione", + "perIterationDesc": "Utilizza un seme diverso per ogni iterazione", + "perPromptLabel": "Per immagine", + "label": "Comportamento del seme" + }, + "maxPrompts": "Numero massimo di prompt", + "dynamicPrompts": "Prompt dinamici", + "promptsPreview": "Anteprima dei prompt", + "showDynamicPrompts": "Mostra prompt dinamici", + "loading": "Generazione prompt dinamici...", + "promptsToGenerate": "Prompt da generare", + "problemGeneratingPrompts": "Problema nella generazione dei prompt" + }, + "popovers": { + "paramScheduler": { + "paragraphs": [ + "Il campionatore utilizzato durante il processo di generazione.", + "Ciascun campionatore definisce come aggiungere in modo iterativo il rumore a un'immagine o come aggiornare un campione in base all'output di un modello." + ], + "heading": "Campionatore" + }, + "compositingMaskAdjustments": { + "heading": "Regolazioni della maschera", + "paragraphs": [ + "Regola la maschera." + ] + }, + "compositingCoherenceMode": { + "heading": "Modalità", + "paragraphs": [ + "Metodo utilizzato per creare un'immagine coerente con l'area mascherata appena generata." + ] + }, + "clipSkip": { + "paragraphs": [ + "Scegli quanti livelli del modello CLIP saltare.", + "Alcuni modelli funzionano meglio con determinate impostazioni di CLIP Skip." + ], + "heading": "CLIP Skip" + }, + "compositingCoherencePass": { + "heading": "Passaggio di Coerenza", + "paragraphs": [ + "Un secondo ciclo di riduzione del rumore aiuta a comporre l'immagine Inpaint/Outpaint." + ] + }, + "paramNegativeConditioning": { + "paragraphs": [ + "Il processo di generazione evita i concetti nel prompt negativo. Utilizzatelo per escludere qualità o oggetti dall'output.", + "Supporta la sintassi e gli incorporamenti di Compel." + ], + "heading": "Prompt negativo" + }, + "compositingBlurMethod": { + "heading": "Metodo di sfocatura", + "paragraphs": [ + "Il metodo di sfocatura applicato all'area mascherata." + ] + }, + "paramPositiveConditioning": { + "heading": "Prompt positivo", + "paragraphs": [ + "Guida il processo di generazione. Puoi usare qualsiasi parola o frase.", + "Supporta sintassi e incorporamenti di Compel e Prompt Dinamici." + ] + }, + "controlNetBeginEnd": { + "heading": "Percentuale passi Inizio / Fine", + "paragraphs": [ + "Questa impostazione determina quale parte del processo di rimozione del rumore (generazione) incorpora la guida da questo livello.", + "• Passo iniziale (%): specifica quando iniziare ad applicare la guida da questo livello durante il processo di generazione.", + "• Passo finale (%): specifica quando interrompere l'applicazione della guida di questo livello e ripristinare la guida generale dal modello e altre impostazioni." + ] + }, + "noiseUseCPU": { + "paragraphs": [ + "Controlla se viene generato rumore sulla CPU o sulla GPU.", + "Con il rumore della CPU abilitato, un seme particolare produrrà la stessa immagine su qualsiasi macchina.", + "Non vi è alcun impatto sulle prestazioni nell'abilitare il rumore della CPU." + ], + "heading": "Usa la CPU per generare rumore" + }, + "scaleBeforeProcessing": { + "paragraphs": [ + "\"Auto\" scala l'area selezionata alla dimensione più adatta al modello prima del processo di generazione dell'immagine.", + "\"Manuale\" consente di scegliere la larghezza e l'altezza a cui verrà ridimensionata l'area selezionata prima del processo di generazione dell'immagine." + ], + "heading": "Scala prima dell'elaborazione" + }, + "paramRatio": { + "heading": "Proporzioni", + "paragraphs": [ + "Le proporzioni delle dimensioni dell'immagine generata.", + "Per i modelli SD1.5 si consiglia una dimensione dell'immagine (in numero di pixel) equivalente a 512x512 mentre per i modelli SDXL si consiglia una dimensione equivalente a 1024x1024." + ] + }, + "dynamicPrompts": { + "paragraphs": [ + "Prompt Dinamici crea molte variazioni a partire da un singolo prompt.", + "La sintassi di base è \"a {red|green|blue} ball\". Ciò produrrà tre prompt: \"a red ball\", \"a green ball\" e \"a blue ball\".", + "Puoi utilizzare la sintassi quante volte vuoi in un singolo prompt, ma assicurati di tenere sotto controllo il numero di prompt generati con l'impostazione \"Numero massimo di prompt\"." + ], + "heading": "Prompt Dinamici" + }, + "paramVAE": { + "paragraphs": [ + "Modello utilizzato per tradurre l'output dell'intelligenza artificiale nell'immagine finale." + ], + "heading": "VAE" + }, + "paramIterations": { + "paragraphs": [ + "Il numero di immagini da generare.", + "Se i prompt dinamici sono abilitati, ciascuno dei prompt verrà generato questo numero di volte." + ], + "heading": "Iterazioni" + }, + "paramVAEPrecision": { + "heading": "Precisione VAE", + "paragraphs": [ + "La precisione utilizzata durante la codifica e decodifica VAE.", + "Fp16/Mezza precisione è più efficiente, a scapito di minori variazioni dell'immagine." + ] + }, + "paramSeed": { + "paragraphs": [ + "Controlla il rumore iniziale utilizzato per la generazione.", + "Disabilita l'opzione \"Casuale\" per produrre risultati identici con le stesse impostazioni di generazione." + ], + "heading": "Seme" + }, + "controlNetResizeMode": { + "heading": "Modalità ridimensionamento", + "paragraphs": [ + "Metodo per adattare le dimensioni dell'immagine di controllo dell'adattatore alle dimensioni di generazione." + ] + }, + "dynamicPromptsSeedBehaviour": { + "paragraphs": [ + "Controlla il modo in cui viene utilizzato il seme durante la generazione dei prompt.", + "Per iterazione utilizzerà un seme univoco per ogni iterazione. Usalo per esplorare variazioni del prompt su un singolo seme.", + "Ad esempio, se hai 5 prompt, ogni immagine utilizzerà lo stesso seme.", + "Per immagine utilizzerà un seme univoco per ogni immagine. Ciò fornisce più variazione." + ], + "heading": "Comportamento del seme" + }, + "paramModel": { + "heading": "Modello", + "paragraphs": [ + "Modello utilizzato per la generazione. Diversi modelli vengono addestrati per specializzarsi nella produzione di risultati e contenuti estetici diversi." + ] + }, + "paramDenoisingStrength": { + "paragraphs": [ + "Controlla la differenza tra l'immagine generata e il/i livello/i raster.", + "Una forza inferiore rimane più vicina ai livelli raster visibili combinati. Una forza superiore si basa maggiormente sul prompt globale.", + "Se non sono presenti livelli raster con contenuto visibile, questa impostazione viene ignorata." + ], + "heading": "Forza di riduzione del rumore" + }, + "dynamicPromptsMaxPrompts": { + "heading": "Numero massimo di prompt", + "paragraphs": [ + "Limita il numero di prompt che possono essere generati da Prompt Dinamici." + ] + }, + "infillMethod": { + "paragraphs": [ + "Metodo di riempimento durante il processo di Outpaint o Inpaint." + ], + "heading": "Metodo di riempimento" + }, + "controlNetWeight": { + "heading": "Peso", + "paragraphs": [ + "Regola la forza con cui il livello influenza il processo di generazione", + "• Peso maggiore (0.75-2): crea un impatto più significativo sul risultato finale.", + "• Peso inferiore (0-0.75): crea un impatto minore sul risultato finale." + ] + }, + "paramCFGScale": { + "heading": "Scala CFG", + "paragraphs": [ + "Controlla quanto il prompt influenza il processo di generazione.", + "Valori elevati della scala CFG possono provocare una saturazione eccessiva e distorsioni nei risultati della generazione. " + ] + }, + "controlNetControlMode": { + "paragraphs": [ + "Attribuisce più peso al prompt oppure a ControlNet." + ], + "heading": "Modalità di controllo" + }, + "paramSteps": { + "heading": "Passi", + "paragraphs": [ + "Numero di passi che verranno eseguiti in ogni generazione.", + "Un numero di passi più elevato generalmente creerà immagini migliori ma richiederà più tempo di generazione." + ] + }, + "lora": { + "heading": "LoRA", + "paragraphs": [ + "Modelli concettuali utilizzati insieme ai modelli di base." + ] + }, + "controlNet": { + "paragraphs": [ + "ControlNet fornisce una guida al processo di generazione, aiutando a creare immagini con composizione, struttura o stile controllati, a seconda del modello selezionato." + ], + "heading": "ControlNet" + }, + "paramCFGRescaleMultiplier": { + "heading": "Moltiplicatore di riscala CFG", + "paragraphs": [ + "Moltiplicatore di riscala per la guida CFG, utilizzato per modelli addestrati utilizzando SNR a terminale zero (ztsnr).", + "Valore suggerito di 0.7 per questi modelli." + ] + }, + "controlNetProcessor": { + "heading": "Processore", + "paragraphs": [ + "Metodo di elaborazione dell'immagine di input per guidare il processo di generazione. Processori diversi forniranno effetti o stili diversi nelle immagini generate." + ] + }, + "imageFit": { + "heading": "Adatta l'immagine iniziale alle dimensioni di output", + "paragraphs": [ + "Ridimensiona l'immagine iniziale in base alla larghezza e all'altezza dell'immagine di output. Si consiglia di abilitarlo." + ] + }, + "loraWeight": { + "heading": "Peso", + "paragraphs": [ + "Peso del LoRA. Un peso maggiore comporterà un impatto maggiore sull'immagine finale." + ] + }, + "paramAspect": { + "heading": "Aspetto", + "paragraphs": [ + "Proporzioni dell'immagine generata. La modifica del rapporto aggiornerà di conseguenza la larghezza e l'altezza.", + "\"Ottimizza\" imposterà la larghezza e l'altezza alle dimensioni ottimali per il modello scelto." + ] + }, + "paramHeight": { + "heading": "Altezza", + "paragraphs": [ + "Altezza dell'immagine generata. Deve essere un multiplo di 8." + ] + }, + "paramHrf": { + "heading": "Abilita correzione alta risoluzione", + "paragraphs": [ + "Genera immagini di alta qualità con una risoluzione maggiore di quella ottimale per il modello. Generalmente utilizzato per impedire la duplicazione nell'immagine generata." + ] + }, + "paramUpscaleMethod": { + "heading": "Metodo di ampliamento", + "paragraphs": [ + "Metodo utilizzato per ampliare l'immagine per la correzione ad alta risoluzione." + ] + }, + "patchmatchDownScaleSize": { + "heading": "Ridimensiona", + "paragraphs": [ + "Quanto ridimensionamento avviene prima del riempimento.", + "Un ridimensionamento più elevato migliorerà le prestazioni e ridurrà la qualità." + ] + }, + "paramWidth": { + "paragraphs": [ + "Larghezza dell'immagine generata. Deve essere un multiplo di 8." + ], + "heading": "Larghezza" + }, + "refinerModel": { + "heading": "Modello Affinatore", + "paragraphs": [ + "Modello utilizzato durante la parte di affinamento del processo di generazione.", + "Simile al modello di generazione." + ] + }, + "refinerNegativeAestheticScore": { + "paragraphs": [ + "Valuta le generazioni in modo che siano più simili alle immagini con un punteggio estetico basso, in base ai dati di addestramento." + ], + "heading": "Punteggio estetico negativo" + }, + "refinerScheduler": { + "paragraphs": [ + "Campionatore utilizzato durante la parte di affinamento del processo di generazione.", + "Simile al campionatore di generazione." + ], + "heading": "Campionatore" + }, + "refinerStart": { + "heading": "Inizio affinamento", + "paragraphs": [ + "A che punto nel processo di generazione inizierà ad essere utilizzato l'affinatore.", + "0 significa che l'affinatore verrà utilizzato per l'intero processo di generazione, 0.8 significa che l'affinatore verrà utilizzato per l'ultimo 20% del processo di generazione." + ] + }, + "refinerSteps": { + "heading": "Passi", + "paragraphs": [ + "Numero di passi che verranno eseguiti durante la parte di affinamento del processo di generazione.", + "Simile ai passi di generazione." + ] + }, + "refinerCfgScale": { + "heading": "Scala CFG", + "paragraphs": [ + "Controlla quanto il prompt influenza il processo di generazione.", + "Simile alla scala CFG di generazione." + ] + }, + "seamlessTilingXAxis": { + "heading": "Piastrella senza giunte sull'asse X", + "paragraphs": [ + "Affianca senza soluzione di continuità un'immagine lungo l'asse orizzontale." + ] + }, + "seamlessTilingYAxis": { + "heading": "Piastrella senza giunte sull'asse Y", + "paragraphs": [ + "Affianca senza soluzione di continuità un'immagine lungo l'asse verticale." + ] + }, + "refinerPositiveAestheticScore": { + "heading": "Punteggio estetico positivo", + "paragraphs": [ + "Valuta le generazioni in modo che siano più simili alle immagini con un punteggio estetico elevato, in base ai dati di addestramento." + ] + }, + "compositingCoherenceMinDenoise": { + "heading": "Livello minimo di riduzione del rumore", + "paragraphs": [ + "Intensità minima di riduzione rumore per la modalità di Coerenza", + "L'intensità minima di riduzione del rumore per la regione di coerenza durante l'inpaint o l'outpaint" + ] + }, + "compositingMaskBlur": { + "paragraphs": [ + "Il raggio di sfocatura della maschera." + ], + "heading": "Sfocatura maschera" + }, + "compositingCoherenceEdgeSize": { + "heading": "Dimensione del bordo", + "paragraphs": [ + "La dimensione del bordo del passaggio di coerenza." + ] + }, + "ipAdapterMethod": { + "heading": "Modalità", + "paragraphs": [ + "La modalità definisce il modo in cui l'immagine di riferimento guiderà il processo di generazione." + ] + }, + "scale": { + "heading": "Scala", + "paragraphs": [ + "La scala controlla la dimensione dell'immagine di uscita e si basa su un multiplo della risoluzione dell'immagine di ingresso. Ad esempio, un ampliamento 2x su un'immagine 1024x1024 produrrebbe in uscita a 2048x2048." + ] + }, + "upscaleModel": { + "paragraphs": [ + "Il modello di ampliamento, scala l'immagine alle dimensioni di uscita prima di aggiungere i dettagli. È possibile utilizzare qualsiasi modello di ampliamento supportato, ma alcuni sono specializzati per diversi tipi di immagini, come foto o disegni al tratto." + ], + "heading": "Modello di ampliamento" + }, + "creativity": { + "heading": "Creatività", + "paragraphs": [ + "La creatività controlla quanta libertà è concessa al modello quando si aggiungono dettagli. Una creatività bassa rimane vicina all'immagine originale, mentre una creatività alta consente più cambiamenti. Quando si usa un prompt, una creatività alta aumenta l'influenza del prompt." + ] + }, + "structure": { + "heading": "Struttura", + "paragraphs": [ + "La struttura determina quanto l'immagine finale rispecchierà lo schema dell'originale. Un valore struttura basso permette cambiamenti significativi, mentre un valore struttura alto conserva la composizione e lo schema originali." + ] + }, + "fluxDevLicense": { + "heading": "Licenza non commerciale", + "paragraphs": [ + "Questo modello è concesso in licenza esclusivamente per uso non commerciale. I modelli FLUX.1 [dev] utilizzano la licenza FLUX.1 [dev] Non-Commercial, mentre FLUX.2 Klein 9B utilizza la licenza FLUX.2 Non-Commercial." + ] + }, + "optimizedDenoising": { + "heading": "Immagine-a-immagine ottimizzata", + "paragraphs": [ + "Abilita 'Immagine-a-immagine ottimizzata' per una scala di riduzione del rumore più graduale per le trasformazioni da immagine a immagine e di inpaint con modelli Flux. Questa impostazione migliora la capacità di controllare la quantità di modifica applicata a un'immagine, ma può essere disattivata se preferisci usare la scala di riduzione rumore standard. Questa impostazione è ancora in fase di messa a punto ed è in stato beta." + ] + }, + "paramGuidance": { + "heading": "Guida", + "paragraphs": [ + "Controlla quanto il prompt influenza il processo di generazione.", + "Valori di guida elevati possono causare sovrasaturazione e una guida elevata o bassa può causare risultati di generazione distorti. La guida si applica solo ai modelli FLUX DEV." + ] + }, + "regionalReferenceImage": { + "paragraphs": [ + "Pennello per applicare un'immagine di riferimento ad aree specifiche." + ], + "heading": "Immagine di riferimento Regionale" + }, + "rasterLayer": { + "paragraphs": [ + "Contenuto basato sui pixel della tua tela, utilizzato durante la generazione dell'immagine." + ], + "heading": "Livello Raster" + }, + "regionalGuidance": { + "heading": "Guida Regionale", + "paragraphs": [ + "Pennello per guidare la posizione in cui devono apparire gli elementi dei prompt globali." + ] + }, + "regionalGuidanceAndReferenceImage": { + "heading": "Guida regionale e immagine di riferimento regionale", + "paragraphs": [ + "Per la Guida Regionale, utilizzare il pennello per indicare dove devono apparire gli elementi dei prompt globali.", + "Per l'immagine di riferimento regionale, utilizzare il pennello per applicare un'immagine di riferimento ad aree specifiche." + ] + }, + "globalReferenceImage": { + "heading": "Immagine di riferimento Globale", + "paragraphs": [ + "Applica un'immagine di riferimento per influenzare l'intera generazione." + ] + }, + "inpainting": { + "paragraphs": [ + "Controlla quale area viene modificata, in base all'intensità di riduzione del rumore." + ] + }, + "tileSize": { + "heading": "Dimensione riquadro", + "paragraphs": [ + "Controlla la dimensione dei riquadri utilizzati durante il processo di ampliamento. Riquadri più grandi consumano più memoria, ma possono produrre risultati migliori.", + "I modelli SD1.5 hanno un valore predefinito di 768, mentre i modelli SDXL hanno un valore predefinito di 1024. Ridurre le dimensioni dei riquadri in caso di problemi di memoria." + ] + }, + "tileOverlap": { + "heading": "Sovrapposizione riquadri", + "paragraphs": [ + "Controlla la sovrapposizione tra riquadri adiacenti durante l'ampliamento. Valori di sovrapposizione più elevati aiutano a ridurre le giunzioni visibili tra i riquadri, ma consuma più memoria.", + "Il valore predefinito di 128 è adatto alla maggior parte dei casi, ma è possibile modificarlo in base alle proprie esigenze specifiche e ai limiti di memoria." + ] + }, + "colorCompensation": { + "paragraphs": [ + "Regola l'immagine di input per ridurre le variazioni di colore durante l'inpainting o img2img (solo SDXL)." + ], + "heading": "Compensazione del colore" + }, + "seedVarianceRandomizePercent": { + "paragraphs": [ + "Percentuale di valori di incorporamento che ricevono rumore (1-100%).", + "Valori più bassi creano modelli di rumore più selettivi, mentre il 100% influisce su tutti i valori in egual misura." + ], + "heading": "Percentuale di variazione" + }, + "seedVarianceStrength": { + "paragraphs": [ + "Controlla l'intensità del rumore aggiunto agli embedding. L'intensità viene calibrata automaticamente in base alla deviazione standard dell'embedding.", + "Valori inferiori a 0.1 produrranno variazioni lievi, che aumenteranno fino a diventare più marcate a 0,5. Valori superiori a 0.5 potrebbero portare a risultati inaspettati." + ], + "heading": "Intensità della varianza" + }, + "seedVarianceEnhancer": { + "paragraphs": [ + "Z-Image-Turbo può produrre immagini relativamente simili con semi diversi. Questa funzionalità aggiunge rumore basato sui semi agli embedding di testo per aumentare la variabilità visiva mantenendo la riproducibilità.", + "Abilita questa opzione per ottenere risultati più diversificati quando esplori semi diversi." + ], + "heading": "Potenziamento varianza del seme" + }, + "fluxDypePreset": { + "paragraphs": [ + "L'estrapolazione dinamica della posizione (DyPE) migliora la qualità della generazione FLUX a risoluzioni superiori alla dimensione di addestramento (1024px).", + "Off: generazione standard. Auto: abilita automaticamente per immagini > 1536px. 4K: impostazioni ottimizzate per output con risoluzione 4K." + ], + "heading": "DyPE (alta risoluzione)" + }, + "fluxDypeScale": { + "paragraphs": [ + "Controlla l'entità della modulazione DyPE. Valori più alti = estrapolazione più forte.", + "Predefinito: 2.0. Intervallo: 0.0-8.0." + ], + "heading": "DyPE Scala (λs)" + }, + "fluxDypeExponent": { + "paragraphs": [ + "Controlla l'intensità dell'effetto dinamico nel tempo.", + "2.0: Consigliato per risoluzioni 4K+. Programmazione aggressiva con transizioni rapide per la pulizia degli artefatti.", + "1.0: Buon punto di partenza per risoluzioni ~2K-3K.", + "0.5: Programma più delicato per risoluzioni appena superiori a quelle native (1024px)." + ], + "heading": "DyPE Esponente (λt)" + }, + "cpuOnly": { + "paragraphs": [ + "Se abilitato, solo il componente codificatore del testo verrà eseguito sulla CPU anziché sulla GPU.", + "Ciò consente di risparmiare VRAM per il denoiser, con un impatto minimo sulle prestazioni. Le uscite di condizionamento vengono automaticamente trasferite alla GPU per il denoiser." + ], + "heading": "Solo CPU" + }, + "fp8Storage": { + "heading": "Archiviazione FP8", + "paragraphs": [ + "Memorizza i pesi del modello in formato FP8 nella VRAM, riducendo l'utilizzo della memoria di circa il 50% rispetto a FP16.", + "Durante l'inferenza, i pesi vengono convertiti strato per strato alla precisione di calcolo (FP16/BF16), preservando così la qualità dell'immagine. Funziona su tutte le GPU CUDA." + ] + } + }, + "sdxl": { + "scheduler": "Campionatore", + "noModelsAvailable": "Nessun modello disponibile", + "denoisingStrength": "Forza di riduzione del rumore", + "loading": "Caricamento...", + "steps": "Passi", + "refinerStart": "Inizio Affinamento", + "cfgScale": "Scala CFG", + "refiner": "Affinatore", + "negAestheticScore": "Punteggio estetico negativo", + "refinermodel": "Modello Affinatore", + "posAestheticScore": "Punteggio estetico positivo", + "refinerSteps": "Passi Affinamento", + "concatPromptStyle": "Collegamento di prompt e stile", + "freePromptStyle": "Prompt manuale Stile", + "negStylePrompt": "Prompt di stile negativo", + "posStylePrompt": "Prompt di stile positivo" + }, + "metadata": { + "positivePrompt": "Prompt positivo", + "negativePrompt": "Prompt negativo", + "generationMode": "Modalità generazione", + "Threshold": "Livello di soglia del rumore", + "metadata": "Metadati", + "strength": "Forza Immagine a Immagine", + "seed": "Seme", + "imageDetails": "Dettagli dell'immagine", + "model": "Modello", + "noImageDetails": "Nessun dettaglio dell'immagine trovato", + "cfgScale": "Scala CFG", + "height": "Altezza", + "noMetaData": "Nessun metadato trovato", + "width": "Larghezza", + "createdBy": "Creato da", + "workflow": "Flusso di lavoro", + "steps": "Passi", + "scheduler": "Campionatore", + "recallParameters": "Richiama i parametri", + "noRecallParameters": "Nessun parametro da richiamare trovato", + "cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)", + "allPrompts": "Tutti i prompt", + "imageDimensions": "Dimensioni dell'immagine", + "parameterSet": "Parametro {{parameter}} impostato", + "canvasV2Metadata": "Livelli Tela", + "guidance": "Guida", + "seamlessXAxis": "Asse X senza giunte", + "seamlessYAxis": "Asse Y senza giunte", + "vae": "VAE", + "parsingFailed": "Analisi non riuscita", + "recallParameter": "Richiama {{label}}", + "seedVarianceRandomizePercent": "Casualità della varianza del seme %", + "seedVarianceEnabled": "Varianza seme abilitata", + "seedVarianceStrength": "Intensità della varianza del seme", + "geminiTemperature": "Gemini Temperatura", + "geminiThinkingLevel": "Gemini Livello di ragionamento", + "openaiQuality": "OpenAI Qualità", + "openaiInputFidelity": "OpenAI Fedeltà Input", + "imageSize": "Dimensioni immagine", + "openaiBackground": "OpenAI Sfondo", + "seedreamWatermark": "Filigrana Seedream", + "seedreamOptimizePrompt": "Seedream Ottimizza Prompt" + }, + "hrf": { + "metadata": { + "strength": "Forza della Correzione Alta Risoluzione", + "enabled": "Correzione Alta Risoluzione Abilitata", + "method": "Metodo della Correzione Alta Risoluzione" + }, + "hrf": "Correzione Alta Risoluzione", + "enableHrf": "Abilita correzione ad alta risoluzione", + "upscaleMethod": "Metodo di ampliamento" + }, + "workflows": { + "saveWorkflowAs": "Salva flusso di lavoro come", + "workflowEditorMenu": "Menu dell'editor del flusso di lavoro", + "workflowName": "Nome del flusso di lavoro", + "saveWorkflow": "Salva flusso di lavoro", + "workflowLibrary": "Libreria flussi di lavoro", + "workflowSaved": "Flusso di lavoro salvato", + "unnamedWorkflow": "Flusso di lavoro senza nome", + "savingWorkflow": "Salvataggio del flusso di lavoro...", + "loading": "Caricamento dei flussi di lavoro", + "problemSavingWorkflow": "Problema durante il salvataggio del flusso di lavoro", + "deleteWorkflow": "Elimina flusso di lavoro", + "workflows": "Flussi di lavoro", + "newWorkflowCreated": "Nuovo flusso di lavoro creato", + "downloadWorkflow": "Salva su file", + "uploadWorkflow": "Carica da file", + "noWorkflows": "Nessun flusso di lavoro", + "workflowCleared": "Flusso di lavoro cancellato", + "saveWorkflowToProject": "Salva flusso di lavoro nel progetto", + "descending": "Discendente", + "created": "Creato", + "ascending": "Ascendente", + "name": "Nome", + "updated": "Aggiornato", + "opened": "Aperto", + "convertGraph": "Converti grafico", + "loadWorkflow": "$t(common.load) Flusso di lavoro", + "autoLayout": "Schema automatico", + "loadFromGraph": "Carica il flusso di lavoro dal grafico", + "chooseWorkflowFromLibrary": "Scegli il flusso di lavoro dalla libreria", + "deleteWorkflow2": "Vuoi davvero eliminare questo flusso di lavoro? Questa operazione non può essere annullata.", + "edit": "Modifica", + "download": "Scarica", + "copyShareLink": "Copia Condividi Link", + "copyShareLinkForWorkflow": "Copia Condividi Link del Flusso di lavoro", + "delete": "Elimina", + "builder": { + "resetAllNodeFields": "Reimposta tutti i campi del nodo", + "row": "Riga", + "nodeField": "Campo del nodo", + "slider": "Cursore", + "emptyRootPlaceholderEditMode": "Per iniziare, trascina qui un elemento del modulo o un campo nodo.", + "containerPlaceholder": "Contenitore vuoto", + "headingPlaceholder": "Titolo vuoto", + "column": "Colonna", + "nodeFieldTooltip": "Per aggiungere un campo nodo, fare clic sul piccolo pulsante con il segno più sul campo nell'editor del flusso di lavoro, oppure trascinare il campo in base al suo nome nel modulo.", + "label": "Etichetta", + "deleteAllElements": "Elimina tutti gli elementi del modulo", + "addToForm": "Aggiungi al Modulo", + "layout": "Schema", + "builder": "Generatore Modulo", + "zoomToNode": "Zoom sul nodo", + "component": "Componente", + "showDescription": "Mostra Descrizione", + "singleLine": "Linea singola", + "multiLine": "Linea Multipla", + "both": "Entrambi", + "textPlaceholder": "Testo vuoto", + "heading": "Intestazione", + "divider": "Divisore", + "container": "Contenitore", + "text": "Testo", + "numberInput": "Ingresso numerico", + "containerRowLayout": "Contenitore (disposizione riga)", + "containerColumnLayout": "Contenitore (disposizione colonna)", + "minimum": "Minimo", + "maximum": "Massimo", + "dropdown": "Elenco a discesa", + "addOption": "Aggiungi opzione", + "resetOptions": "Reimposta opzioni", + "publish": "Pubblica", + "workflowLocked": "Flusso di lavoro bloccato", + "workflowLockedDuringPublishing": "Il flusso di lavoro è bloccato durante la configurazione per la pubblicazione.", + "selectOutputNode": "Seleziona nodo di uscita", + "changeOutputNode": "Cambia nodo di uscita", + "publishedWorkflowOutputs": "Uscite", + "noPublishableInputs": "Nessun ingresso pubblicabile", + "published": "Pubblicato", + "cannotPublish": "Impossibile pubblicare il flusso di lavoro", + "noOutputNodeSelected": "Nessun nodo di uscita selezionato", + "unpublish": "Annulla pubblicazione", + "workflowLockedPublished": "I flussi di lavoro pubblicati sono bloccati per la modifica.\nPuoi annullare la pubblicazione del flusso di lavoro per modificarlo o crearne una copia.", + "publishedWorkflowInputs": "Ingressi", + "unpublishableInputs": "Questi input non pubblicabili verranno omessi", + "publishWarnings": "Avvertenze", + "errorWorkflowHasUnsavedChanges": "Il flusso di lavoro presenta modifiche non salvate", + "errorWorkflowHasInvalidGraph": "Grafico del flusso di lavoro non valido (passare il mouse sul pulsante Invoke per i dettagli)", + "errorWorkflowHasNoOutputNode": "Nessun nodo di uscita selezionato", + "warningWorkflowHasUnpublishableInputFields": "Il flusso di lavoro presenta alcuni ingressi non pubblicabili: questi verranno omessi dal flusso di lavoro pubblicato", + "publishFailed": "Pubblicazione non riuscita", + "publishFailedDesc": "Si è verificato un problema durante la pubblicazione del flusso di lavoro. Riprova.", + "publishSuccess": "Il tuo flusso di lavoro è in fase di pubblicazione", + "publishSuccessDesc": "Controlla il pannello di controllo del progetto per verificarne l'avanzamento.", + "publishedWorkflowIsLocked": "Il flusso di lavoro pubblicato è bloccato", + "publishingValidationRun": "Esecuzione della convalida della pubblicazione", + "publishingValidationRunInProgress": "È in corso la convalida della pubblicazione.", + "publishedWorkflowsLocked": "I flussi di lavoro pubblicati sono bloccati e non possono essere modificati o eseguiti. Annulla la pubblicazione del flusso di lavoro o salva una copia per modificare o eseguire questo flusso di lavoro.", + "warningWorkflowHasNoPublishableInputFields": "Nessun campo di ingresso pubblicabile selezionato: il flusso di lavoro pubblicato verrà eseguito solo con i valori predefiniti", + "publishInProgress": "Pubblicazione in corso", + "selectingOutputNode": "Selezione del nodo di uscita", + "selectingOutputNodeDesc": "Fare clic su un nodo per selezionarlo come nodo di uscita del flusso di lavoro.", + "errorWorkflowHasUnpublishableNodes": "Il flusso di lavoro ha nodi di estrazione lotto, generatore o metadati", + "showShuffle": "Mostra Mescola", + "shuffle": "Mescola", + "removeFromForm": "Rimuovi dal modulo", + "emptyRootPlaceholderViewMode": "Fare clic su Modifica per iniziare a creare un modulo per questo flusso di lavoro.", + "workflowBuilderAlphaWarning": "Il generatore di flussi di lavoro è attualmente in versione alpha. Potrebbero esserci modifiche sostanziali prima della versione stabile." + }, + "loadMore": "Carica altro", + "searchPlaceholder": "Cerca per nome, descrizione o etichetta", + "shared": "Condiviso", + "browseWorkflows": "Sfoglia i flussi di lavoro", + "saveChanges": "Salva modifiche", + "yourWorkflows": "I tuoi flussi di lavoro", + "recentlyOpened": "Aperto di recente", + "workflowThumbnail": "Miniatura del flusso di lavoro", + "private": "Privato", + "deselectAll": "Deseleziona tutto", + "view": "Visualizza", + "recommended": "Consigliato per te", + "emptyStringPlaceholder": "", + "published": "Pubblicato", + "defaultWorkflows": "Flussi di lavoro predefiniti", + "userWorkflows": "Flussi di lavoro dell'utente", + "projectWorkflows": "Flussi di lavoro del progetto", + "allLoaded": "Tutti i flussi di lavoro caricati", + "filterByTags": "Filtra per etichetta", + "noRecentWorkflows": "Nessun flusso di lavoro recente", + "openWorkflow": "Apri flusso di lavoro", + "problemLoading": "Problema nel caricamento dei flussi di lavoro", + "noDescription": "Nessuna descrizione", + "searchWorkflows": "Ricerca flussi di lavoro", + "clearWorkflowSearchFilter": "Cancella filtro di ricerca del flusso di lavoro", + "openLibrary": "Apri libreria", + "tags": "Etichette", + "sharedWorkflows": "Flussi di lavoro condivisi", + "shareWorkflow": "Flusso di lavoro condiviso" + }, + "accordions": { + "compositing": { + "infillTab": "Riempimento", + "coherenceTab": "Passaggio di coerenza", + "title": "Composizione" + }, + "control": { + "title": "Controllo" + }, + "generation": { + "title": "Generazione" + }, + "advanced": { + "title": "Avanzate", + "options": "Opzioni $t(accordions.advanced.title)" + }, + "image": { + "title": "Immagine" + } + }, + "prompt": { + "compatibleEmbeddings": "Incorporamenti compatibili", + "addPromptTrigger": "Aggiungi Trigger nel prompt", + "noMatchingTriggers": "Nessun Trigger corrispondente", + "discard": "Scarta", + "replace": "Sostituisci", + "expandingPrompt": "Espansione del prompt...", + "uploadImageForPromptGeneration": "Carica l'immagine per la generazione del prompt", + "expandCurrentPrompt": "Espandi il prompt corrente", + "generateFromImage": "Genera prompt dall'immagine", + "resultTitle": "Espansione del prompt completata", + "resultSubtitle": "Scegli come gestire il prompt espanso:", + "insert": "Inserisci", + "noPromptHistory": "Nessuna cronologia di prompt registrata.", + "noMatchingPrompts": "Nessun prompt corrispondente nella cronologia.", + "toSwitchBetweenPrompts": "per passare da un prompt all'altro.", + "promptHistory": "Cronologia dei prompt", + "clearHistory": "Cancella cronologia", + "usePrompt": "Utilizza il prompt", + "searchPrompts": "Ricerca...", + "imageToPrompt": "Immagine a prompt", + "selectVisionModel": "Seleziona il modello di visione...", + "changeImage": "Cambia immagine", + "uploadImage": "Carica immagine", + "generatePrompt": "Genera prompt", + "expandPromptWithLLM": "Espandi il prompt con LLM", + "expandPrompt": "Espandi il prompt", + "selectTextLLM": "Seleziona LLM testuale...", + "expand": "Espandi", + "noTextLLMInstalledTitle": "Nessun modello LLM testuale installato", + "noTextLLMInstalledDescription": "L'espansione del prompt richiede un modello linguistico causale (LLM) di tipo testuale. Consigliamo Qwen2.5-1.5B-Instruct (~3 GB): è piccolo, veloce e disponibile come modello di partenza.", + "noVisionModelInstalledTitle": "Nessun modello di visione installato", + "noVisionModelInstalledDescription": "La funzione di conversione immagine-a-prompt richiede un modello di linguaggio visivo (ad esempio LLaVA Onevision). Il pacchetto iniziale da 0,5 byte (~1 GB) è quello predefinito più leggero.", + "openModelManager": "Apri Gestione Modelli" + }, + "controlLayers": { + "addLayer": "Aggiungi Livello", + "moveToFront": "Sposta in primo piano", + "moveToBack": "Sposta in fondo", + "moveForward": "Sposta avanti", + "moveBackward": "Sposta indietro", + "autoNegative": "Auto Negativo", + "rectangle": "Rettangolo", + "addPositivePrompt": "Aggiungi $t(controlLayers.prompt)", + "addNegativePrompt": "Aggiungi $t(controlLayers.negativePrompt)", + "regionalGuidance": "Guida regionale", + "opacity": "Opacità", + "mergeVisible": "Fondi il visibile", + "mergeVisibleOk": "Livelli uniti", + "deleteReferenceImage": "Elimina l'immagine di riferimento", + "referenceImage": "Immagine di riferimento", + "fitBboxToLayers": "Adatta il riquadro di delimitazione ai livelli", + "mergeVisibleError": "Errore durante l'unione dei livelli", + "regionalReferenceImage": "Immagine di riferimento Regionale", + "newLayerFromImage": "Nuovo livello da immagine", + "newCanvasFromImage": "Nuova tela da immagine", + "globalReferenceImage": "Immagine di riferimento Globale", + "copyToClipboard": "Copia negli appunti", + "clearHistory": "Cancella la cronologia", + "inpaintMask": "Maschera Inpaint", + "controlLayer": "Livello di Controllo", + "rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)", + "rasterLayer_withCount_many": "Livelli Raster", + "rasterLayer_withCount_other": "Livelli Raster", + "controlLayer_withCount_one": "$t(controlLayers.controlLayer)", + "controlLayer_withCount_many": "Livelli di controllo", + "controlLayer_withCount_other": "Livelli di controllo", + "clipToBbox": "Ritaglia i tratti al riquadro", + "duplicate": "Duplica", + "width": "Larghezza", + "addControlLayer": "Aggiungi $t(controlLayers.controlLayer)", + "addInpaintMask": "Aggiungi $t(controlLayers.inpaintMask)", + "addRegionalGuidance": "Aggiungi $t(controlLayers.regionalGuidance)", + "addRasterLayer": "Aggiungi $t(controlLayers.rasterLayer)", + "clearCaches": "Svuota le cache", + "regionIsEmpty": "La regione selezionata è vuota", + "recalculateRects": "Ricalcola rettangoli", + "removeBookmark": "Rimuovi segnalibro", + "saveCanvasToGallery": "Salva la tela nella Galleria", + "regional": "Regionale", + "global": "Globale", + "canvas": "Tela", + "bookmark": "Segnalibro per cambio rapido", + "newRegionalReferenceImageOk": "Immagine di riferimento regionale creata", + "newRegionalReferenceImageError": "Problema nella creazione dell'immagine di riferimento regionale", + "newControlLayerOk": "Livello di controllo creato", + "bboxOverlay": "Mostra sovrapposizione riquadro", + "outputOnlyMaskedRegions": "In uscita solo le regioni generate", + "enableAutoNegative": "Abilita Auto Negativo", + "disableAutoNegative": "Disabilita Auto Negativo", + "showHUD": "Mostra HUD", + "maskFill": "Riempimento maschera", + "addReferenceImage": "Aggiungi $t(controlLayers.referenceImage)", + "sendToCanvas": "Invia alla Tela", + "saveBboxToGallery": "Salva il riquadro di delimitazione nella Galleria", + "cropLayerToBbox": "Ritaglia il livello al riquadro di delimitazione", + "savedToGalleryError": "Errore durante il salvataggio nella galleria", + "rasterLayer": "Livello Raster", + "regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)", + "regionalGuidance_withCount_many": "Guide regionali", + "regionalGuidance_withCount_other": "Guide regionali", + "inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)", + "inpaintMask_withCount_many": "Maschere Inpaint", + "inpaintMask_withCount_other": "Maschere Inpaint", + "savedToGalleryOk": "Salvato nella Galleria", + "newGlobalReferenceImageOk": "Immagine di riferimento globale creata", + "newGlobalReferenceImageError": "Problema nella creazione dell'immagine di riferimento globale", + "newControlLayerError": "Problema nella creazione del livello di controllo", + "newRasterLayerOk": "Livello raster creato", + "newRasterLayerError": "Problema nella creazione del livello raster", + "saveLayerToAssets": "Salva il livello nelle Risorse", + "pullBboxIntoLayerError": "Problema nel caricare il riquadro nel livello", + "pullBboxIntoReferenceImageOk": "Contenuto del riquadro inserito nell'immagine di riferimento", + "pullBboxIntoLayerOk": "Riquadro caricato nel livello", + "pullBboxIntoReferenceImageError": "Problema nell'inserimento del contenuto del riquadro nell'immagine di riferimento", + "controlMode": { + "balanced": "Bilanciato (consigliato)", + "controlMode": "Modalità di controllo", + "prompt": "Prompt", + "control": "Controllo", + "megaControl": "Mega Controllo" + }, + "negativePrompt": "Prompt Negativo", + "prompt": "Prompt Positivo", + "beginEndStepPercentShort": "Inizio/Fine %", + "ipAdapterMethod": { + "full": "Stile e Composizione", + "style": "Stile (semplice)", + "composition": "Solo Composizione", + "ipAdapterMethod": "Modalità", + "fullDesc": "Applica lo stile visivo (colori, texture) e la composizione (disposizione, struttura).", + "styleDesc": "Applica lo stile visivo (colori, texture) senza considerare la disposizione. Precedentemente chiamato \"Solo stile\".", + "compositionDesc": "Replica disposizione e struttura ignorando lo stile di riferimento.", + "styleStrong": "Stile (forte)", + "styleStrongDesc": "Applica uno stile visivo forte, con un'influenza sulla composizione leggermente ridotta.", + "stylePrecise": "Stile (preciso)", + "stylePreciseDesc": "Applica uno stile visivo preciso, eliminando l'influenza del soggetto." + }, + "showingType": "Mostra {{type}}", + "dynamicGrid": "Griglia dinamica", + "tool": { + "view": "Muovi", + "colorPicker": "Selettore Colore", + "rectangle": "Rettangolo", + "bbox": "Riquadro di delimitazione", + "move": "Sposta", + "brush": "Pennello", + "eraser": "Cancellino", + "gradient": "Gradiente", + "text": "Testo", + "lasso": "Lazo", + "shapes": "Forme" + }, + "filter": { + "apply": "Applica", + "reset": "Reimposta", + "process": "Elabora", + "cancel": "Annulla", + "autoProcess": "Processo automatico", + "filterType": "Tipo Filtro", + "filter": "Filtro", + "filters": "Filtri", + "mlsd_detection": { + "score_threshold": "Soglia di punteggio", + "distance_threshold": "Soglia di distanza", + "description": "Genera una mappa dei segmenti di linea dal livello selezionato utilizzando il modello di rilevamento dei segmenti di linea MLSD.", + "label": "Rilevamento segmenti di linea" + }, + "content_shuffle": { + "label": "Mescola contenuto", + "scale_factor": "Fattore di scala", + "description": "Mescola il contenuto del livello selezionato, in modo simile all'effetto \"liquefa\"." + }, + "mediapipe_face_detection": { + "min_confidence": "Confidenza minima", + "label": "Rilevamento del volto MediaPipe", + "max_faces": "Max volti", + "description": "Rileva i volti nel livello selezionato utilizzando il modello di rilevamento dei volti MediaPipe." + }, + "dw_openpose_detection": { + "draw_face": "Disegna il volto", + "description": "Rileva le pose umane nel livello selezionato utilizzando il modello DW Openpose.", + "label": "Rilevamento DW Openpose", + "draw_hands": "Disegna le mani", + "draw_body": "Disegna il corpo" + }, + "normal_map": { + "description": "Genera una mappa delle normali dal livello selezionato.", + "label": "Mappa delle normali" + }, + "lineart_edge_detection": { + "label": "Rilevamento bordi Lineart", + "coarse": "Grossolano", + "description": "Genera una mappa dei bordi dal livello selezionato utilizzando il modello di rilevamento dei bordi Lineart." + }, + "depth_anything_depth_estimation": { + "model_size_small": "Piccolo", + "model_size_small_v2": "Piccolo v2", + "model_size": "Dimensioni modello", + "model_size_large": "Grande", + "model_size_base": "Base", + "description": "Genera una mappa di profondità dal livello selezionato utilizzando un modello Depth Anything." + }, + "color_map": { + "label": "Mappa colore", + "description": "Crea una mappa dei colori dal livello selezionato.", + "tile_size": "Dimens. Piastrella" + }, + "canny_edge_detection": { + "high_threshold": "Soglia superiore", + "low_threshold": "Soglia inferiore", + "description": "Genera una mappa dei bordi dal livello selezionato utilizzando l'algoritmo di rilevamento dei bordi Canny.", + "label": "Rilevamento bordi Canny" + }, + "spandrel_filter": { + "scale": "Scala di destinazione", + "autoScaleDesc": "Il modello selezionato verrà eseguito fino al raggiungimento della scala di destinazione.", + "description": "Esegue un modello immagine-a-immagine sul livello selezionato.", + "label": "Modello Immagine-a-Immagine", + "model": "Modello", + "autoScale": "Auto Scala" + }, + "pidi_edge_detection": { + "quantize_edges": "Quantizza i bordi", + "scribble": "Scarabocchio", + "description": "Genera una mappa dei bordi dal livello selezionato utilizzando il modello di rilevamento dei bordi PiDiNet.", + "label": "Rilevamento bordi PiDiNet" + }, + "hed_edge_detection": { + "label": "Rilevamento bordi HED", + "description": "Genera una mappa dei bordi dal livello selezionato utilizzando il modello di rilevamento dei bordi HED.", + "scribble": "Scarabocchio" + }, + "lineart_anime_edge_detection": { + "description": "Genera una mappa dei bordi dal livello selezionato utilizzando il modello di rilevamento dei bordi Lineart Anime.", + "label": "Rilevamento bordi Lineart Anime" + }, + "forMoreControl": "Per un maggiore controllo, fare clic su Avanzate qui sotto.", + "advanced": "Avanzate", + "processingLayerWith": "Elaborazione del livello con il filtro {{type}}.", + "img_blur": { + "label": "Sfoca immagine", + "description": "Sfoca il livello selezionato.", + "blur_type": "Tipo di sfocatura", + "blur_radius": "Raggio", + "gaussian_type": "Gaussiana" + }, + "img_noise": { + "size": "Dimensione del rumore", + "salt_and_pepper_type": "Sale e pepe", + "gaussian_type": "Gaussiano", + "noise_color": "Rumore colorato", + "description": "Aggiunge rumore al livello selezionato.", + "noise_type": "Tipo di rumore", + "label": "Aggiungi rumore", + "noise_amount": "Quantità" + }, + "adjust_image": { + "description": "Regola il canale selezionato di un'immagine.", + "alpha": "Alfa (RGBA)", + "label": "Regola l'immagine", + "blue": "Blu (RGBA)", + "luminosity": "Luminosità (LAB)", + "channel": "Canale", + "value_setting": "Valore", + "scale_values": "Scala i valori", + "red": "Rosso (RGBA)", + "green": "Verde (RGBA)", + "cyan": "Ciano (CMYK)", + "magenta": "Magenta (CMYK)", + "yellow": "Giallo (CMYK)", + "black": "Nero (CMYK)", + "hue": "Tonalità (HSV)", + "saturation": "Saturazione (HSV)", + "value": "Valore (HSV)" + }, + "pbr_maps": { + "label": "Crea mappe PBR" + } + }, + "fill": { + "grid": "Griglia", + "crosshatch": "Tratteggio incrociato", + "fillColor": "Colore di riempimento", + "fillStyle": "Stile riempimento", + "solid": "Solido", + "vertical": "Verticale", + "horizontal": "Orizzontale", + "diagonal": "Diagonale", + "bgFillColor": "Colore di sfondo", + "fgFillColor": "Colore di primo piano", + "switchColors": "Commuta FG/BG (X)" + }, + "locked": "Bloccato", + "hidingType": "Nascondere {{type}}", + "logDebugInfo": "Registro Info Debug", + "layer_one": "Livello", + "layer_many": "Livelli", + "layer_other": "Livelli", + "disableTransparencyEffect": "Disabilita l'effetto trasparenza", + "transparency": "Trasparenza", + "unlocked": "Sbloccato", + "enableTransparencyEffect": "Abilita l'effetto trasparenza", + "replaceLayer": "Sostituisci livello", + "pullBboxIntoLayer": "Carica l'immagine delimitata nel riquadro", + "pullBboxIntoReferenceImage": "Carica l'immagine delimitata nel riquadro", + "showProgressOnCanvas": "Mostra i progressi sulla Tela", + "weight": "Peso", + "deleteSelected": "Elimina selezione", + "settings": { + "isolatedStagingPreview": "Anteprima di generazione isolata", + "isolatedPreview": "Anteprima isolata", + "invertBrushSizeScrollDirection": "Inverti scorrimento per dimensione pennello", + "snapToGrid": { + "label": "Aggancia alla griglia", + "on": "Acceso", + "off": "Spento" + }, + "pressureSensitivity": "Sensibilità alla pressione", + "preserveMask": { + "alert": "Preservare la regione mascherata", + "label": "Preserva la regione mascherata" + }, + "isolatedLayerPreview": "Anteprima livello isolato", + "isolatedLayerPreviewDesc": "Se visualizzare solo questo livello quando si eseguono operazioni come il filtraggio o la trasformazione.", + "saveAllImagesToGallery": { + "alert": "Invia le nuove generazioni alla Galleria, bypassando la Tela", + "label": "Invia le nuove generazioni alla Galleria" + } + }, + "transform": { + "reset": "Reimposta", + "fitToBbox": "Adatta al Riquadro", + "transform": "Trasforma", + "apply": "Applica", + "cancel": "Annulla", + "fitMode": "Adattamento", + "fitModeContain": "Contieni", + "fitModeFill": "Riempi", + "fitModeCover": "Copri", + "smoothingMode": "Modalità di ricampionamento", + "smoothingDesc": "Applica un ricampionamento di alta qualità lato backend alla conferma delle trasformazioni.", + "smoothing": "Smussamento", + "smoothingModeBilinear": "Bilineare", + "smoothingModeBicubic": "Bicubico" + }, + "stagingArea": { + "next": "Successiva", + "discard": "Scarta", + "discardAll": "Scarta tutto", + "accept": "Accetta", + "saveToGallery": "Salva nella Galleria", + "previous": "Precedente", + "showResultsOn": "Visualizzare i risultati", + "showResultsOff": "Nascondere i risultati", + "hideThumbnails": "Nascondi le miniature", + "showThumbnails": "Mostra miniature" + }, + "HUD": { + "bbox": "Riquadro di delimitazione", + "entityStatus": { + "isHidden": "{{title}} è nascosto", + "isLocked": "{{title}} è bloccato", + "isTransforming": "{{title}} sta trasformando", + "isFiltering": "{{title}} sta filtrando", + "isEmpty": "{{title}} è vuoto", + "isDisabled": "{{title}} è disabilitato" + }, + "scaledBbox": "Riquadro scalato", + "textSessionActive": "L'inserimento del testo è attivo" + }, + "canvasContextMenu": { + "newControlLayer": "Nuovo Livello di Controllo", + "newRegionalReferenceImage": "Nuova immagine di riferimento Regionale", + "newGlobalReferenceImage": "Nuova immagine di riferimento Globale", + "bboxGroup": "Crea dal riquadro di delimitazione", + "saveBboxToGallery": "Salva il riquadro nella Galleria", + "cropCanvasToBbox": "Ritaglia la Tela al riquadro", + "canvasGroup": "Tela", + "newRasterLayer": "Nuovo Livello Raster", + "saveCanvasToGallery": "Salva la Tela nella Galleria", + "saveToGalleryGroup": "Salva nella Galleria", + "newInpaintMask": "Nuova maschera Inpaint", + "newRegionalGuidance": "Nuova Guida Regionale", + "copyToClipboard": "Copia negli appunti", + "copyCanvasToClipboard": "Copia la tela negli appunti", + "copyBboxToClipboard": "Copia il riquadro di delimitazione negli appunti", + "newResizedControlLayer": "Nuovo livello di controllo ridimensionato" + }, + "copyRasterLayerTo": "Copia $t(controlLayers.rasterLayer) in", + "copyControlLayerTo": "Copia $t(controlLayers.controlLayer) in", + "copyInpaintMaskTo": "Copia $t(controlLayers.inpaintMask) in", + "selectObject": { + "dragToMove": "Trascina un punto per spostarlo", + "clickToAdd": "Fare clic sul livello per aggiungere un punto", + "clickToRemove": "Clicca su un punto per rimuoverlo", + "pointType": "Tipo punto", + "apply": "Applica", + "reset": "Reimposta", + "cancel": "Annulla", + "selectObject": "Seleziona oggetto", + "invertSelection": "Inverti selezione", + "exclude": "Escludi", + "include": "Includi", + "neutral": "Neutro", + "saveAs": "Salva come", + "process": "Elabora", + "desc": "Seleziona un singolo oggetto di destinazione. Una volta completata la selezione, fai clic su Applica per eliminare tutto ciò che si trova al di fuori dell'area selezionata, oppure salva la selezione come nuovo livello.", + "visualModeDesc": "La modalità visiva utilizza input di tipo riquadro e punto per selezionare un oggetto.", + "visualMode1": "Fai clic e trascina per disegnare un riquadro attorno all'oggetto che desideri selezionare. Puoi ottenere risultati migliori disegnando il riquadro un po' più grande o più piccolo dell'oggetto.", + "visualMode2": "Fai clic per aggiungere un punto verde includi oppure fai clic tenendo premuto il tasto Maiusc per aggiungere un punto rosso escludi per indicare al modello cosa includere o escludere.", + "visualMode3": "I punti possono essere utilizzati per perfezionare una selezione di caselle oppure in modo indipendente.", + "promptModeDesc": "La modalità Prompt utilizza l'input di testo per selezionare un oggetto.", + "promptMode1": "Digitare una breve descrizione dell'oggetto che si desidera selezionare.", + "promptMode2": "Utilizzare un linguaggio semplice, evitando descrizioni complesse o oggetti multipli.", + "model": "Modello", + "prompt": "Prompt di selezione" + }, + "convertControlLayerTo": "Converti $t(controlLayers.controlLayer) in", + "newRasterLayer": "Nuovo $t(controlLayers.rasterLayer)", + "newRegionalGuidance": "Nuova $t(controlLayers.regionalGuidance)", + "convertInpaintMaskTo": "Converti $t(controlLayers.inpaintMask) in", + "copyRegionalGuidanceTo": "Copia $t(controlLayers.regionalGuidance) in", + "convertRasterLayerTo": "Converti $t(controlLayers.rasterLayer) in", + "convertRegionalGuidanceTo": "Converti $t(controlLayers.regionalGuidance) in", + "newControlLayer": "Nuovo $t(controlLayers.controlLayer)", + "newInpaintMask": "Nuova $t(controlLayers.inpaintMask)", + "mergeDown": "Unire in basso", + "mergingLayers": "Unione dei livelli", + "controlLayerEmptyState": "Carica un'immagine, trascina un'immagine dalla galleria su questo livello, trascina il riquadro di delimitazione in questo livello oppure disegna sulla tela per iniziare.", + "useImage": "Usa immagine", + "resetGenerationSettings": "Ripristina impostazioni di generazione", + "referenceImageEmptyState": "Per iniziare, carica un'immagine oppure trascina un'immagine dalla galleria su questa Immagine di riferimento.", + "asRasterLayer": "Come $t(controlLayers.rasterLayer)", + "asRasterLayerResize": "Come $t(controlLayers.rasterLayer) (Ridimensiona)", + "asControlLayer": "Come $t(controlLayers.controlLayer)", + "asControlLayerResize": "Come $t(controlLayers.controlLayer) (Ridimensiona)", + "newSession": "Nuova sessione", + "resetCanvasLayers": "Ripristina livelli Tela", + "referenceImageRegional": "Immagine di riferimento (regionale)", + "warnings": { + "controlAdapterNoModelSelected": "nessun modello selezionato per il livello di controllo", + "controlAdapterNoControl": "nessun controllo selezionato/disegnato", + "ipAdapterNoModelSelected": "nessun modello di immagine di riferimento selezionato", + "rgNoPromptsOrIPAdapters": "nessun prompt testuale o immagini di riferimento", + "rgReferenceImagesNotSupported": "Immagini di riferimento regionali non supportate per il modello base selezionato", + "rgNoRegion": "nessuna regione disegnata", + "problemsFound": "Problemi riscontrati", + "unsupportedModel": "livello non supportato per il modello base selezionato", + "controlAdapterIncompatibleBaseModel": "modello di base del livello di controllo incompatibile", + "rgNegativePromptNotSupported": "Prompt negativo non supportato per il modello base selezionato", + "ipAdapterIncompatibleBaseModel": "modello base dell'immagine di riferimento incompatibile", + "ipAdapterNoImageSelected": "nessuna immagine di riferimento selezionata", + "rgAutoNegativeNotSupported": "Auto-Negativo non supportato per il modello base selezionato", + "fluxFillIncompatibleWithControlLoRA": "Il controllo LoRA non è compatibile con FLUX Fill", + "bboxHidden": "Il riquadro di delimitazione è nascosto (Shift+o per attivarlo)" + }, + "pasteTo": "Incolla su", + "pasteToBboxDesc": "Nuovo livello (nel riquadro di delimitazione)", + "pasteToAssets": "Risorse", + "copyRegionError": "Errore durante la copia di {{region}}", + "pasteToAssetsDesc": "Incolla in Risorse", + "pasteToBbox": "Riquadro di delimitazione", + "pasteToCanvas": "Tela", + "pasteToCanvasDesc": "Nuovo livello (nella Tela)", + "regionCopiedToClipboard": "{{region}} Copiato negli appunti", + "errors": { + "unableToFindImage": "Impossibile trovare l'immagine", + "unableToLoadImage": "Impossibile caricare l'immagine" + }, + "fluxReduxImageInfluence": { + "high": "Alta", + "low": "Basso", + "imageInfluence": "Influenza dell'immagine", + "lowest": "Il più basso", + "medium": "Medio", + "highest": "La più alta" + }, + "denoiseLimit": "Limite di riduzione del rumore", + "addImageNoise": "Aggiungi $t(controlLayers.imageNoise)", + "addDenoiseLimit": "Aggiungi $t(controlLayers.denoiseLimit)", + "imageNoise": "Rumore dell'immagine", + "exportCanvasToPSD": "Esporta la tela in PSD", + "ruleOfThirds": "Mostra la regola dei terzi", + "showNonRasterLayers": "Mostra livelli non raster (Shift+H)", + "hideNonRasterLayers": "Nascondi livelli non raster (Shift+H)", + "referenceImageEmptyStateWithCanvasOptions": "Carica un'immagine, trascina un'immagine dalla galleria su questa immagine di riferimento o trascina il riquadro di delimitazione in questa immagine di riferimento per iniziare.", + "autoSwitch": { + "off": "Spento", + "switchOnStart": "All'inizio", + "switchOnFinish": "Alla fine", + "doNotAutoSwitch": "Non commutare automaticamente", + "switchOnStartDesc": "Attiva all'avvio", + "switchOnFinishDesc": "Attiva al termine" + }, + "invertMask": "Inverti maschera", + "fitBboxToMasks": "Adatta il riquadro di delimitazione alle maschere", + "maxRefImages": "Max Immagini di rif.to", + "useAsReferenceImage": "Usa come immagine di riferimento", + "globalReferenceImage_withCount_one": "$t(controlLayers.globalReferenceImage)", + "globalReferenceImage_withCount_many": "Immagini di riferimento globali", + "globalReferenceImage_withCount_other": "Immagini di riferimento globali", + "layer_withCount_one": "Livello ({{count}})", + "layer_withCount_many": "Livelli ({{count}})", + "layer_withCount_other": "Livelli ({{count}})", + "addAdjustments": "Aggiungi regolazioni", + "removeAdjustments": "Rimuovi regolazioni", + "adjustments": { + "simple": "Semplice", + "curves": "Curve", + "heading": "Regolazioni", + "expand": "Espandi regolazioni", + "collapse": "Comprimi regolazioni", + "brightness": "Luminosità", + "contrast": "Contrasto", + "saturation": "Saturazione", + "temperature": "Temperatura", + "tint": "Tinta", + "sharpness": "Nitidezza", + "reset": "Reimposta", + "master": "Composito", + "finish": "Applica" + }, + "deletePrompt": "Elimina prompt", + "addGlobalReferenceImage": "Aggiungi $t(controlLayers.globalReferenceImage)", + "referenceImageGlobal": "Immagine di riferimento (globale)", + "sendingToGallery": "Invia generazioni alla Galleria", + "sendToGallery": "Invia alla Galleria", + "sendToGalleryDesc": "Premendo Invoke viene generata e salvata un'immagine unica nella tua galleria.", + "newImg2ImgCanvasFromImage": "Nuovo immagine-a-immagine da Immagine", + "sendToCanvasDesc": "Premendo Invoke il lavoro in corso viene visualizzato sulla tela.", + "viewProgressOnCanvas": "Visualizza i progressi e gli output nel Visualizzatore immagini.", + "regionalGuidance_withCount_hidden": "Guida regionale ({{count}} nascosti)", + "controlLayers_withCount_hidden": "Livelli di controllo ({{count}} nascosti)", + "rasterLayers_withCount_hidden": "Livelli raster ({{count}} nascosti)", + "globalReferenceImages_withCount_hidden": "Immagini di riferimento globali ({{count}} nascoste)", + "inpaintMasks_withCount_hidden": "Maschere Inpaint ({{count}} nascoste)", + "regionalGuidance_withCount_visible": "Guida regionale ({{count}})", + "controlLayers_withCount_visible": "Livelli di controllo ({{count}})", + "rasterLayers_withCount_visible": "Livelli raster ({{count}})", + "globalReferenceImages_withCount_visible": "Immagini di riferimento globali ({{count}})", + "inpaintMasks_withCount_visible": "Maschere Inpaint ({{count}})", + "pastedTo": "Incollato su {{destination}}", + "stagingOnCanvas": "Predisponi le immagini su", + "newGallerySession": "Nuova sessione della Galleria", + "newGallerySessionDesc": "Questo cancellerà la tela e tutte le impostazioni, ad eccezione della selezione del modello. Le generazioni verranno inviate alla galleria.", + "newCanvasSession": "Nuova sessione Tela", + "newCanvasSessionDesc": "Questo cancellerà la tela e tutte le impostazioni, ad eccezione della selezione del modello. Le generazioni verranno predisposte sulla tela.", + "replaceCurrent": "Sostituisci l'attuale", + "uploadOrDragAnImage": "Trascina un'immagine dalla galleria o carica un'immagine.", + "sendingToCanvas": "Predisponi le generazioni sulla Tela", + "viewProgressInViewer": "Visualizza i progressi e gli output nel Visualizzatore immagini.", + "extractMaskedAreaMissingData": "Impossibile estrarre: mancano i dati dell'immagine o della maschera.", + "extractMaskedAreaFailed": "Impossibile estrarre l'area mascherata.", + "maskLayerEmpty": "Il livello maschera è vuoto", + "extractRegion": "Estrai regione", + "compositeOperation": { + "label": "Modalità di fusione", + "add": "Aggiungi modalità di fusione", + "remove": "Rimuovi modalità di fusione", + "blendModes": { + "color": "Colore", + "hue": "Tonalità", + "source-over": "Normale", + "overlay": "Sovrapponi", + "soft-light": "Luce soffusa", + "hard-light": "Luce intensa", + "screen": "Schermo", + "color-burn": "Brucia colore", + "color-dodge": "Schiarisci colore", + "multiply": "Moltiplica", + "darken": "Scurisci", + "lighten": "Schiarisci", + "difference": "Differenza", + "luminosity": "Luminosità", + "saturation": "Saturazione" + } + }, + "booleanOps": { + "label": "Operazioni booleane", + "intersect": "Intersezione", + "cutout": "Ritaglia", + "cutaway": "Taglia via", + "exclude": "Escludi" + }, + "gradient": { + "linear": "Lineare", + "radial": "Radiale", + "clip": "Ritaglia gradiente" + }, + "text": { + "font": "Carattere", + "size": "Dimensione", + "bold": "Grassetto", + "italic": "Italico", + "underline": "Sottolineato", + "strikethrough": "Barrato", + "alignLeft": "Allinea a sinistra", + "alignCenter": "Allinea al centro", + "alignRight": "Allinea a destra", + "lineHeight": "Spaziatura", + "lineHeightDense": "Densa", + "lineHeightNormal": "Normale", + "lineHeightSpacious": "Spaziosa" + }, + "workflowIntegration": { + "title": "Eseguire il flusso di lavoro sula Tela", + "description": "Seleziona un flusso di lavoro con un nodo Output su tela e un parametro immagine da eseguire sul livello corrente della tela. Puoi regolare i parametri prima dell'esecuzione. Il risultato verrà aggiunto nuovamente alla tela.", + "execute": "Eseguire il flusso di lavoro", + "executing": "Esecuzione in corso...", + "runWorkflow": "Avvia il flusso di lavoro", + "filteringWorkflows": "Filtraggio dei flussi di lavoro...", + "loadingWorkflows": "Caricamento dei flussi di lavoro...", + "noWorkflowsFound": "Nessun flusso di lavoro trovato.", + "noWorkflowsWithImageField": "Nessun flusso di lavoro compatibile trovato. Un flusso di lavoro richiede un Generatore Modello con un campo di input immagine e un nodo Output su tela.", + "selectWorkflow": "Seleziona il flusso di lavoro", + "selectPlaceholder": "Scegli un flusso di lavoro...", + "unnamedWorkflow": "Flusso di lavoro senza nome", + "loadingParameters": "Caricamento dei parametri del flusso di lavoro in corso...", + "noFormBuilderError": "Questo flusso di lavoro non dispone di un generatore di moduli e non può essere utilizzato. Selezionare un flusso di lavoro diverso.", + "imageFieldSelected": "Questo campo riceverà l'immagine della tela", + "imageFieldNotSelected": "Fai clic su questo campo per usarlo per l'immagine sulla tela", + "executionStarted": "L'esecuzione del flusso di lavoro è stata avviata", + "executionStartedDescription": "Il risultato apparirà nell'area di lavoro una volta completata l'operazione.", + "executionFailed": "Impossibile eseguire il flusso di lavoro" + }, + "disableReferenceImage": "Disabilita l'immagine di riferimento", + "enableReferenceImage": "Abilita l'immagine di riferimento", + "invertRegion": "Inverti la regione", + "invalidReferenceImage": "Immagine di riferimento non valida:", + "removeImageFromCollection": "Rimuovi l'immagine dalla raccolta", + "selectRefImage": "Seleziona l'immagine di riferimento", + "canvasProject": { + "project": "Progetto", + "saveProject": "Salva il progetto Tela", + "loadProject": "Carica il progetto Tela", + "saveSuccess": "Progetto salvato", + "saveSuccessDesc": "Progetto salvato con {{count}} immagini", + "saveError": "Impossibile salvare il progetto", + "loadSuccess": "Progetto caricato", + "loadSuccessDesc": "Stato della tela ripristinato dal file di progetto", + "loadError": "Impossibile caricare il progetto", + "loadWarning": "Il caricamento di un progetto sostituirà l'area di lavoro corrente, inclusi tutti i livelli, le maschere, le immagini di riferimento e i parametri di generazione. Questa operazione è irreversibile.", + "projectName": "Nome del progetto" + }, + "lasso": { + "freehand": "A mano libera", + "polygon": "Poligono", + "polygonHint": "Fai clic per aggiungere punti, fai clic sul primo punto per chiudere." + }, + "transparencyLocked": "Trasparenza bloccata", + "transparencyUnlocked": "Trasparenza sbloccata", + "snapshot": { + "snapshots": "Salva o carica l'istantanea della tela", + "saveSnapshot": "Salva istantanea", + "restoreSnapshot": "Ripristina istantanea", + "snapshotNamePlaceholder": "Nome dell'istantanea", + "save": "Salva", + "delete": "Elimina", + "snapshotSaved": "Istantanea \"{{name}}\" salvata", + "snapshotRestored": "Istantanea \"{{name}}\" ripristinata", + "snapshotDeleted": "Istantanea \"{{name}}\" eliminata", + "snapshotSaveFailed": "Impossibile salvare l'istantanea", + "snapshotRestoreFailed": "Impossibile ripristinare l'istantanea", + "snapshotDeleteFailed": "Impossibile eliminare l'istantanea", + "snapshotMissingImages_one": "{{count}} immagine a cui fa riferimento questa istantanea non esiste più e verrà visualizzata come segnaposto", + "snapshotMissingImages_many": "{{count}} immagini a cui fa riferimento questa istantanea non esistono più e verranno visualizzate come segnaposto", + "snapshotMissingImages_other": "{{count}} immagini a cui fa riferimento questa istantanea non esistono più e verranno visualizzate come segnaposto", + "snapshotIncompatible": "Questa istantanea è stata creata con una versione diversa e non è più compatibile", + "overwriteSnapshotTitle": "Sovrascrivere l'istantanea?", + "overwriteSnapshotMessage": "Esiste già un'istantanea denominata \"{{name}}\". Si desidera sovrascriverla?", + "overwrite": "Sovrascrivi" + }, + "modifierHints": { + "keys": { + "option": "Opzione", + "shift": "Maiusc", + "space": "Spazio", + "wheel": "Rotellina", + "arrows": "Frecce", + "enter": "Invio" + }, + "labels": { + "pan": "Panoramica", + "pickColor": "Scegli il colore", + "straightLine": "Linea retta", + "resizeBrush": "Ridimensiona il pennello", + "resizeEraser": "Ridimensiona la gomma", + "snap45Degrees": "Ruota di 45 gradi", + "lockAspectRatio": "Blocca le proporzioni", + "unlockAspectRatio": "Sblocca le proporzioni", + "scaleFromCenter": "Scala dal centro", + "fineGrid": "Griglia fine", + "commitText": "Conferma", + "newLine": "Nuova linea", + "cancelText": "Annulla", + "dragText": "Trascina il testo", + "snapRotation": "Rotazione a scatto", + "moveShape": "Sposta la forma", + "erase": "Cancella", + "nudgeSelection": "Sposta selezione" + } + }, + "shape": { + "rect": "Rett", + "oval": "Ovale" + } + }, + "ui": { + "tabs": { + "canvas": "Tela", + "workflows": "Flussi di lavoro", + "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)", + "models": "Modelli", + "modelsTab": "$t(ui.tabs.models) $t(common.tab)", + "queue": "Coda", + "upscaling": "Amplia", + "upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)", + "gallery": "Galleria", + "generate": "Genera", + "customNodes": "Nodi" + }, + "launchpad": { + "workflowsTitle": "Approfondisci i flussi di lavoro.", + "upscalingTitle": "Amplia e aggiungi dettagli.", + "canvasTitle": "Modifica e perfeziona sulla tela.", + "generateTitle": "Genera immagini da prompt testuali.", + "modelGuideText": "Vuoi scoprire quali prompt funzionano meglio per ciascun modello?", + "modelGuideLink": "Consulta la nostra guida ai modelli.", + "workflows": { + "description": "I flussi di lavoro sono modelli riutilizzabili che automatizzano le attività di generazione delle immagini, consentendo di eseguire rapidamente operazioni complesse e di ottenere risultati coerenti.", + "learnMoreLink": "Scopri di più sulla creazione di flussi di lavoro", + "browseTemplates": { + "title": "Sfoglia i modelli di flusso di lavoro", + "description": "Scegli tra flussi di lavoro predefiniti per le attività comuni" + }, + "createNew": { + "title": "Crea un nuovo flusso di lavoro", + "description": "Avvia un nuovo flusso di lavoro da zero" + }, + "loadFromFile": { + "title": "Carica flusso di lavoro da file", + "description": "Carica un flusso di lavoro per iniziare con una configurazione esistente" + }, + "descriptionMultiuser": "I flussi di lavoro sono modelli riutilizzabili che automatizzano le attività di generazione di immagini, consentendo di eseguire rapidamente operazioni complesse e ottenere risultati coerenti. È possibile condividere i flussi di lavoro con altri utenti del sistema selezionando \"Flusso di lavoro condiviso\" durante la creazione o la modifica." + }, + "upscaling": { + "uploadImage": { + "title": "Carica l'immagine da ampliare", + "description": "Fai clic o trascina un'immagine per ingrandirla (JPG, PNG, WebP fino a 100 MB)" + }, + "replaceImage": { + "title": "Sostituisci l'immagine corrente", + "description": "Fai clic o trascina una nuova immagine per sostituire quella corrente" + }, + "imageReady": { + "title": "Immagine pronta", + "description": "Premere Invoke per iniziare l'ampliamento" + }, + "readyToUpscale": { + "title": "Pronto per ampliare!", + "description": "Configura le impostazioni qui sotto, quindi fai clic sul pulsante Invoke per iniziare ad ampliare l'immagine." + }, + "upscaleModel": "Modello per l'ampliamento", + "model": "Modello", + "scale": "Scala", + "helpText": { + "promptAdvice": "Durante l'ampliamento, utilizza un prompt che descriva il mezzo e lo stile. Evita di descrivere dettagli specifici del contenuto dell'immagine.", + "styleAdvice": "L'ampliamento funziona meglio con lo stile generale dell'immagine." + }, + "creativityAndStructure": { + "title": "Creatività e struttura predefinite", + "conservative": "Conservativo", + "balanced": "Bilanciato", + "creative": "Creativo", + "artistic": "Artistico" + } + }, + "createNewWorkflowFromScratch": "Crea un nuovo flusso di lavoro da zero", + "browseAndLoadWorkflows": "Sfoglia e carica i flussi di lavoro esistenti", + "addStyleRef": { + "title": "Aggiungi un riferimento di stile", + "description": "Aggiungi un'immagine per trasferirne l'aspetto." + }, + "editImage": { + "title": "Modifica immagine", + "description": "Aggiungi un'immagine da perfezionare." + }, + "generateFromText": { + "title": "Genera da testo", + "description": "Inserisci un prompt e genera." + }, + "useALayoutImage": { + "description": "Aggiungi un'immagine per controllare la composizione.", + "title": "Usa una immagine guida" + }, + "generate": { + "canvasCalloutTitle": "Vuoi avere più controllo, modificare e affinare le tue immagini?", + "canvasCalloutLink": "Per ulteriori funzionalità, vai su Tela." + } + }, + "panels": { + "launchpad": "Rampa di lancio", + "workflowEditor": "Editor del flusso di lavoro", + "imageViewer": "Visualizzatore", + "canvas": "Tela" + } + }, + "upscaling": { + "creativity": "Creatività", + "structure": "Struttura", + "upscaleModel": "Modello di ampliamento", + "scale": "Scala", + "missingModelsWarning": "Visita Gestione modelli per installare i modelli richiesti:", + "mainModelDesc": "Modello principale (architettura SD1.5 o SDXL)", + "tileControlNetModelDesc": "Modello Tile ControlNet per l'architettura del modello principale scelto", + "upscaleModelDesc": "Modello per l'ampliamento (immagine a immagine)", + "missingUpscaleInitialImage": "Immagine iniziale mancante per l'ampliamento", + "missingUpscaleModel": "Modello per l’ampliamento mancante", + "missingTileControlNetModel": "Nessun modello ControlNet Tile valido installato", + "postProcessingModel": "Modello di post-elaborazione", + "postProcessingMissingModelWarning": "Visita Gestione modelli per installare un modello di post-elaborazione (da immagine a immagine).", + "exceedsMaxSize": "Le impostazioni di ampliamento superano il limite massimo delle dimensioni", + "exceedsMaxSizeDetails": "Il limite massimo di ampliamento è {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixel. Prova un'immagine più piccola o diminuisci la scala selezionata.", + "upscale": "Amplia", + "incompatibleBaseModel": "Architettura del modello principale non supportata per l'ampliamento", + "incompatibleBaseModelDesc": "L'ampliamento è supportato solo per i modelli di architettura SD1.5 e SDXL. Cambia il modello principale per abilitare l'ampliamento.", + "tileControl": "Controllo del riquadro", + "tileSize": "Dimensione del riquadro", + "tileOverlap": "Sovrapposizione riquadro", + "missingModelsWarningNonAdmin": "Chiedi al tuo amministratore di InvokeAI () di installare i modelli richiesti:" + }, + "stylePresets": { + "active": "Attivo", + "choosePromptTemplate": "Scegli un modello di prompt", + "clearTemplateSelection": "Cancella selezione modello", + "copyTemplate": "Copia modello", + "createPromptTemplate": "Crea modello di prompt", + "defaultTemplates": "Modelli predefiniti", + "deleteImage": "Elimina immagine", + "deleteTemplate": "Elimina modello", + "editTemplate": "Modifica modello", + "flatten": "Unisci il modello selezionato al prompt corrente", + "insertPlaceholder": "Inserisci segnaposto", + "myTemplates": "I miei modelli", + "name": "Nome", + "negativePrompt": "Prompt Negativo", + "noMatchingTemplates": "Nessun modello corrispondente", + "promptTemplatesDesc1": "I modelli di prompt aggiungono testo ai prompt che scrivi nelle caselle dei prompt.", + "promptTemplatesDesc3": "Se si omette il segnaposto, il modello verrà aggiunto alla fine del prompt.", + "positivePrompt": "Prompt Positivo", + "preview": "Anteprima", + "private": "Privato", + "searchByName": "Cerca per nome", + "shared": "Condiviso", + "sharedTemplates": "Modelli condivisi", + "templateDeleted": "Modello di prompt eliminato", + "toggleViewMode": "Attiva/disattiva visualizzazione", + "uploadImage": "Carica immagine", + "useForTemplate": "Usa per modello di prompt", + "viewList": "Visualizza l'elenco dei modelli", + "viewModeTooltip": "Ecco come apparirà il tuo prompt con il modello attualmente selezionato. Per modificare il tuo prompt, clicca in un punto qualsiasi della casella di testo.", + "deleteTemplate2": "Vuoi davvero eliminare questo modello? Questa operazione non può essere annullata.", + "unableToDeleteTemplate": "Impossibile eliminare il modello di prompt", + "updatePromptTemplate": "Aggiorna il modello di prompt", + "type": "Tipo", + "promptTemplatesDesc2": "Utilizza la stringa segnaposto
{{placeholder}}
per specificare dove inserire il tuo prompt nel modello.", + "importTemplates": "Importa modelli di prompt (CSV/JSON)", + "exportDownloaded": "Esportazione completata", + "exportFailed": "Impossibile generare e scaricare il file CSV", + "exportPromptTemplates": "Esporta i miei modelli di prompt (CSV)", + "positivePromptColumn": "'prompt' o 'positive_prompt'", + "noTemplates": "Nessun modello", + "acceptedColumnsKeys": "Colonne/chiavi accettate:", + "promptTemplateCleared": "Modello di prompt cancellato", + "togglePromptPreviews": "Attiva/disattiva le anteprime dei prompt", + "noMatchingPresets": "Nessuna preimpostazione corrispondente", + "selectPreset": "Seleziona stile predefinito" + }, + "newUserExperience": { + "gettingStartedSeries": "Desideri maggiori informazioni? Consulta la nostra Getting Started Series per suggerimenti su come sfruttare appieno il potenziale di Invoke Studio.", + "toGetStarted": "Per iniziare, inserisci un prompt nella casella e fai clic su Invoke per generare la tua prima immagine. Seleziona un modello di prompt per migliorare i risultati. Puoi scegliere di salvare le tue immagini direttamente nella Galleria o modificarle nella Tela.", + "noModelsInstalled": "Sembra che non hai installato alcun modello! Puoi scaricare un pacchetto di modelli di avvio o importare modelli.", + "toGetStartedLocal": "Per iniziare, assicurati di scaricare o importare i modelli necessari per eseguire Invoke. Quindi, inserisci un prompt nella casella e fai clic su Invoke per generare la tua prima immagine. Seleziona un modello di prompt per migliorare i risultati. Puoi scegliere di salvare le tue immagini direttamente nella Galleria o modificarle nella Tela.", + "lowVRAMMode": "Per prestazioni ottimali, segui la nostra guida per bassa VRAM.", + "toGetStartedWorkflow": "Per iniziare, compila i campi a sinistra e premi Invoke per generare la tua immagine. Vuoi esplorare altri flussi di lavoro? Fai clic sull'icona della cartella accanto al titolo del flusso di lavoro per visualizzare un elenco di altri modelli che puoi provare.", + "toGetStartedNonAdmin": "Per iniziare, chiedi al tuo amministratore di InvokeAI () di installare i modelli AI necessari per eseguire Invoke. Quindi, inserisci un prompt nella casella e fai clic su Invoke per generare la tua prima immagine. Seleziona un modello di prompt per migliorare i risultati. Puoi scegliere di salvare le immagini direttamente nella Galleria o modificarle nella Tela.", + "noModelsInstalledAskAdmin": "Chiedi al tuo amministratore di installarne alcuni." + }, + "whatsNew": { + "whatsNewInInvoke": "Novità in Invoke", + "readReleaseNotes": "Leggi le note di rilascio", + "watchRecentReleaseVideos": "Guarda i video su questa versione", + "items": [ + "Strumenti di prompt LLM: utilizza modelli linguistici locali per espandere i prompt o generarli da immagini. Installa un modello LLM di testo (ad esempio Qwen2.5-1.5B-Instruct) per iniziare.", + "Supporto per FLUX.2 Klein: InvokeAI ora supporta i nuovi modelli FLUX.2 Klein (varianti 4B e 9B) nei formati GGUF, FP8 e Diffusers. Le funzionalità includono conversione da testo a immagine, da immagine a immagine, inpainting e outpainting. Consulta la sezione \"Modelli di base\" per iniziare.", + "Il supporto DyPE per i modelli FLUX migliora le immagini ad alta risoluzione (>1536 px fino a 4K). Vai alla sezione \"Opzioni avanzate\" per attivarlo.", + "Diversità di Z-Image Turbo: Attiva \"Migliora varianza seme\" in \"Opzioni avanzate\" per aumentare la diversità delle tue generazioni con ZiT.", + "La modalità multiutente supporta più utenti isolati sullo stesso server.", + "Supporto migliorato per i modelli Z-Image e FLUX.2.", + "Numerosi miglioramenti all'interfaccia utente e nuove funzionalità per la tela." + ], + "watchUiUpdatesOverview": "Guarda la panoramica degli aggiornamenti dell'interfaccia utente", + "takeUserSurvey": "📣 Facci sapere cosa ne pensi di InvokeAI. Partecipa al nostro sondaggio sull'esperienza utente!" + }, + "system": { + "logLevel": { + "info": "Info", + "warn": "Avviso", + "fatal": "Fatale", + "error": "Errore", + "debug": "Debug", + "trace": "Traccia", + "logLevel": "Livello di registro" + }, + "logNamespaces": { + "workflows": "Flussi di lavoro", + "generation": "Generazione", + "canvas": "Tela", + "config": "Configurazione", + "models": "Modelli", + "gallery": "Galleria", + "queue": "Coda", + "events": "Eventi", + "system": "Sistema", + "metadata": "Metadati", + "logNamespaces": "Elementi del registro", + "dnd": "Trascina e rilascia" + }, + "enableLogging": "Abilita la registrazione" + }, + "supportVideos": { + "gettingStarted": "Iniziare", + "supportVideos": "Video di supporto", + "watch": "Guarda", + "studioSessionsDesc": "Unisciti al nostro per partecipare alle sessioni live e porre domande. Le sessioni vengono caricate nella playlist la settimana successiva.", + "videos": { + "gettingStarted": { + "title": "Introduzione a Invoke", + "description": "Serie video completa che copre tutto ciò che devi sapere per iniziare a usare Invoke, dalla creazione della tua prima immagine alle tecniche avanzate." + }, + "studioSessions": { + "title": "Sessioni in studio", + "description": "Sessioni approfondite che esplorano le funzionalità avanzate di Invoke, i flussi di lavoro creativi e le discussioni della community." + } + }, + "gettingStartedPlaylist": "Playlist per iniziare", + "studioSessionsPlaylist": "Playlist delle sessioni in studio" + }, + "modelCache": { + "clear": "Cancella la cache del modello", + "clearSucceeded": "Cache del modello cancellata", + "clearFailed": "Problema durante la cancellazione della cache del modello" + }, + "lora": { + "weight": "Peso", + "removeLoRA": "Rimuovi LoRA" + }, + "auth": { + "login": { + "title": "Accedi a InvokeAI", + "rememberMe": "Ricordami per 7 giorni", + "signIn": "Accedi", + "signingIn": "Accesso in corso...", + "loginFailed": "Accesso non riuscito. Controlla le tue credenziali.", + "sessionExpired": "Le tue credenziali sono scadute. Effettua nuovamente l'accesso per continuare." + }, + "setup": { + "title": "Benvenuti a InvokeAI", + "subtitle": "Configura il tuo account amministratore per iniziare", + "emailHelper": "Questo sarà il tuo nome utente per accedere", + "displayName": "Nome da visualizzare", + "displayNamePlaceholder": "Amministratore", + "displayNameHelper": "Il tuo nome come apparirà nell'applicazione", + "passwordHelper": "Deve contenere almeno 8 caratteri, tra maiuscole, minuscole e numeri", + "passwordTooShort": "La password deve essere lunga almeno 8 caratteri", + "passwordMissingRequirements": "La password deve contenere maiuscole, minuscole e numeri", + "confirmPassword": "Conferma password", + "confirmPasswordPlaceholder": "Conferma password", + "passwordsDoNotMatch": "Le password non corrispondono", + "createAccount": "Crea un account amministratore", + "creatingAccount": "Impostazione in corso...", + "setupFailed": "Installazione non riuscita. Riprova.", + "passwordHelperRelaxed": "Inserisci una password qualsiasi (verrà visualizzata la sua robustezza)" + }, + "userMenu": "Menu utente", + "logout": "Esci", + "adminOnlyFeature": "Questa funzionalità è disponibile solo per gli amministratori.", + "profile": { + "menuItem": "Il mio profilo", + "title": "Il mio profilo", + "emailReadOnly": "L'indirizzo email non può essere modificato", + "displayName": "Nome da visualizzare", + "displayNamePlaceholder": "Il tuo nome", + "changePassword": "Cambiare la password", + "currentPassword": "Password attuale", + "currentPasswordPlaceholder": "Password attuale", + "newPassword": "Nuova password", + "newPasswordPlaceholder": "Nuova password", + "confirmPassword": "Conferma nuova password", + "confirmPasswordPlaceholder": "Conferma nuova password", + "passwordsDoNotMatch": "Le password non corrispondono", + "saveSuccess": "Profilo aggiornato con successo", + "saveFailed": "Impossibile salvare il profilo. Riprova." + }, + "userManagement": { + "menuItem": "Gestione utenti", + "title": "Gestione utenti", + "displayName": "Nome da visualizzare", + "displayNamePlaceholder": "Nome da visualizzare", + "newPassword": "Nuova password", + "newPasswordPlaceholder": "Lasciare vuoto per mantenere la password corrente", + "role": "Ruolo", + "status": "Stato", + "actions": "Azioni", + "isAdmin": "Amministratore", + "user": "Utente", + "you": "Tu", + "createUser": "Crea utente", + "editUser": "Modifica utente", + "deleteUser": "Elimina utente", + "deleteConfirm": "Vuoi davvero eliminare \"{{name}}\"? Questa azione non può essere annullata.", + "generatePassword": "Genera una password complessa", + "showPassword": "Mostra password", + "hidePassword": "Nascondi password", + "activate": "Attiva", + "deactivate": "Disattiva", + "saveFailed": "Impossibile salvare l'utente. Riprova.", + "deleteFailed": "Impossibile eliminare l'utente. Riprova.", + "loadFailed": "Impossibile caricare gli utenti.", + "back": "Indietro", + "cannotDeleteSelf": "Non puoi eliminare il tuo account", + "cannotDeactivateSelf": "Non puoi disattivare il tuo account" + }, + "passwordStrength": { + "weak": "Password debole", + "moderate": "Password moderata", + "strong": "Password forte" + } + }, + "cropper": { + "cropImage": "Ritaglia l'immagine", + "aspectRatio": "Rapporto d'aspetto", + "free": "Libera", + "mouseWheelZoom": "Rotellina del mouse: Zoom", + "spaceDragPan": "Spazio + trascina: Panoramica", + "dragCropBoxToAdjust": "Trascina il riquadro di ritaglio o le maniglie per regolare" + }, + "customNodes": { + "title": "Nodi personalizzati", + "gitUrl": "URL del repository Git", + "gitUrlLabel": "URL del repository", + "install": "Installa", + "installing": "Installazione in corso", + "installSuccess": "Pacchetto nodi installato", + "installTitle": "Installa pacchetto Nodi", + "installFailed": "Installazione non riuscita", + "installError": "Si è verificato un errore imprevisto durante l'installazione.", + "securityWarning": "I nodi personalizzati eseguono codice sul tuo sistema. Installa pacchetti di nodi solo da autori di cui ti fidi. I nodi dannosi potrebbero danneggiare il tuo sistema o compromettere i tuoi dati.", + "installDescription": "Clona il repository nella tua directory nodi. I file del flusso di lavoro (.json) vengono importati nella tua libreria. Le dipendenze Python (requirements.txt o pyproject.toml) NON vengono installate automaticamente: segui la documentazione del pacchetto node per installarle manualmente.", + "dependenciesRequiredTitle": "Installazione manuale delle dipendenze richiesta", + "dependenciesRequiredDescription": "'{{name}}' include un {{file}}. Segui la documentazione del pacchetto di nodi per installare le sue dipendenze Python prima di utilizzare i suoi nodi.", + "uninstall": "Disinstalla", + "reload": "Ricarica", + "reloading": "Ricaricamento in corso", + "noNodePacks": "Nessun pacchetto di nodi personalizzato installato.", + "scanFolder": "Scansiona la cartella", + "scanFolderDescription": "I pacchetti di nodi inseriti nella directory dei nodi vengono rilevati automaticamente all'avvio. Utilizzare il pulsante Ricarica per rilevare i pacchetti appena aggiunti senza riavviare il programma.", + "nodesDirectory": "Cartella nodi", + "installQueue": "Registro di installazione", + "queueEmpty": "Nessuna attività di installazione recente.", + "name": "Nome", + "message": "Messaggio", + "nodeCount_one": "{{count}} nodo", + "nodeCount_many": "{{count}} nodi", + "nodeCount_other": "{{count}} nodi", + "uninstalled": "Disinstallato" + } +} diff --git a/invokeai/frontend/web/public/locales/ja.json b/invokeai/frontend/web/public/locales/ja.json new file mode 100644 index 00000000000..74b490ffb0f --- /dev/null +++ b/invokeai/frontend/web/public/locales/ja.json @@ -0,0 +1,3312 @@ +{ + "common": { + "languagePickerLabel": "言語", + "reportBugLabel": "バグ報告", + "settingsLabel": "設定", + "upload": "アップロード", + "load": "ロード", + "back": "戻る", + "statusDisconnected": "切断済", + "cancel": "キャンセル", + "accept": "採用", + "img2img": "img2img", + "loading": "ロード中", + "githubLabel": "Github", + "hotkeysLabel": "ショートカットキー", + "discordLabel": "Discord", + "nodes": "ワークフロー", + "txt2img": "txt2img", + "postprocessing": "ポストプロセス", + "t2iAdapter": "T2I アダプター", + "communityLabel": "コミュニティ", + "dontAskMeAgain": "次回から確認しない", + "areYouSure": "本当によろしいですか?", + "on": "オン", + "ipAdapter": "IPアダプター", + "auto": "自動", + "openInNewTab": "新しいタブで開く", + "controlNet": "コントロールネット", + "linear": "リニア", + "modelManager": "モデルマネージャー", + "learnMore": "もっと学ぶ", + "random": "ランダム", + "batch": "バッチマネージャー", + "advanced": "高度", + "created": "作成済", + "green": "G", + "blue": "B", + "alpha": "α", + "outpaint": "アウトペイント", + "unknown": "不明", + "updated": "更新済", + "add": "追加", + "ai": "ai", + "copyError": "$t(gallery.copy) エラー", + "data": "データ", + "template": "テンプレート", + "red": "R", + "or": "または", + "checkpoint": "Checkpoint", + "direction": "順序", + "simple": "シンプル", + "save": "保存", + "saveAs": "名前をつけて保存", + "somethingWentWrong": "何かの問題が発生しました", + "details": "詳細", + "inpaint": "inpaint", + "delete": "削除", + "copy": "コピー", + "error": "エラー", + "file": "ファイル", + "folder": "フォルダ", + "input": "インプット", + "format": "形式", + "installed": "インストール済み", + "outputs": "アウトプット", + "unknownError": "未知のエラー", + "orderBy": "表示順:", + "enabled": "有効", + "positivePrompt": "ポジティブプロンプト", + "negativePrompt": "ネガティブプロンプト", + "selected": "選択済み", + "aboutDesc": "Invokeを業務で利用する場合:", + "beta": "Beta", + "disabled": "無効", + "editor": "エディタ", + "safetensors": "Safetensors", + "tab": "タブ", + "toResolve": "解決方法", + "openInViewer": "ビューアで開く", + "placeholderSelectAModel": "モデルを選択", + "clipboard": "クリップボード", + "apply": "適用", + "loadingImage": "画像をロード中", + "off": "オフ", + "view": "ビュー", + "edit": "編集", + "ok": "OK", + "reset": "リセット", + "none": "なし", + "new": "新規", + "close": "閉じる", + "warnings": "警告", + "dontShowMeThese": "次回から表示しない", + "generating": "生成中", + "loadingModel": "モデルをロード中", + "layout": "レイアウト", + "step": "ステップ", + "start": "開始", + "count": "回数", + "end": "終了", + "min": "最小", + "max": "最大", + "values": "値", + "row": "行", + "column": "列", + "board": "ボード", + "seed": "シード", + "combinatorial": "組み合わせ", + "aboutHeading": "想像力をこの手に", + "systemInformation": "システム情報", + "value": "値", + "label": "ラベル", + "saveChanges": "変更を保存", + "error_withCount_other": "{{count}} 個のエラー", + "noMatches": "一致したものがありません", + "model_withCount_other": "{{count}}個のモデル", + "noOptions": "オプションがありません", + "search": "検索", + "clear": "クリア", + "compactView": "コンパクトビュー", + "fullView": "フルビュー", + "options_withCount_other": "{{count}}個のオプション", + "crop": "クロップ", + "removeNegativePrompt": "ネガティブプロンプトを削除", + "addNegativePrompt": "ネガティブプロンプトを追加", + "selectYourModel": "モデルを選択", + "goTo": "移動", + "imageFailedToLoad": "画像を読み込めません", + "localSystem": "ローカルシステム", + "notInstalled": "$t(common.installed) ではありません", + "prevPage": "前のページ", + "nextPage": "次のページ", + "resetToDefaults": "デフォルトをリセット", + "collapseAll": "すべて畳む", + "editName": "名前を編集", + "expandAll": "すべてを展開", + "fitView": "ビューをフィット", + "hex": "16進数", + "minimize": "最小化", + "next": "次へ", + "noMatchingItems": "一致するアイテムがありません", + "notifications": "通知", + "openSlider": "スライダーを開く", + "previous": "前へ", + "removeFromCollection": "コレクションから削除", + "resetView": "ビューをリセット", + "saveToAssets": "アセットに保存", + "settings": "設定", + "toggleRgbHex": "RGB/16進数を切り替え", + "unpin": "ピンを外す", + "zoomIn": "ズームイン", + "zoomOut": "ズームアウト", + "json": "JSON" + }, + "gallery": { + "galleryImageSize": "画像のサイズ", + "gallerySettings": "ギャラリーの設定", + "autoSwitchNewImages": "新しい画像に自動切替", + "copy": "コピー", + "image": "画像", + "autoAssignBoardOnClick": "クリックしたボードに自動追加", + "featuresWillReset": "この画像を削除すると、これらの機能は即座にリセットされます。", + "unstarImage": "スターを外す", + "loading": "ロード中", + "currentlyInUse": "この画像は現在下記の機能を使用しています:", + "drop": "ドロップ", + "dropOrUpload": "ドロップまたはアップロード", + "deleteImage_other": "画像 {{count}} 枚を削除", + "deleteImagePermanent": "削除された画像は復元できません。", + "download": "ダウンロード", + "bulkDownloadRequested": "ダウンロード準備中", + "bulkDownloadRequestedDesc": "ダウンロードの準備中です。しばらくお待ちください。", + "bulkDownloadRequestFailed": "ダウンロード準備中に問題が発生", + "bulkDownloadFailed": "ダウンロード失敗", + "alwaysShowImageSizeBadge": "画像サイズバッジを常に表示", + "dropToUpload": "$t(gallery.drop) してアップロード", + "noImageSelected": "画像が選択されていません", + "deleteSelection": "選択中のものを削除", + "downloadSelection": "選択中のものをダウンロード", + "starImage": "スター", + "viewerImage": "閲覧画像", + "compareImage": "比較画像", + "openInViewer": "ビューアで開く", + "selectForCompare": "比較対象として選択", + "slider": "スライダー", + "sideBySide": "横並び", + "hover": "ホバー", + "swapImages": "画像を入れ替える", + "stretchToFit": "画面に合わせる", + "exitCompare": "比較を終了する", + "compareHelp1": "Alt キーを押しながらギャラリーの画像をクリックするか、矢印キーを使用して比較する画像を変更します。", + "compareHelp3": "Cを押して、比較した画像を入れ替えます。", + "compareHelp4": "[Z]または[Esc]を押して終了します。", + "compareHelp2": "M キーを押して比較モードを切り替えます。", + "move": "移動", + "exitSearch": "画像検索を終了", + "oldestFirst": "最古から", + "showStarredImagesFirst": "スター付きを先頭", + "exitBoardSearch": "ボード検索を終了", + "showArchivedBoards": "アーカイブされたボードを表示", + "searchImages": "メタデータで検索", + "gallery": "ギャラリー", + "newestFirst": "最新から", + "go": "進む", + "sortDirection": "並び順", + "displayBoardSearch": "ボード検索", + "displaySearch": "画像を検索", + "boardsSettings": "ボード設定", + "imagesSettings": "ギャラリー設定", + "selectAllOnPage": "ページ上のすべてを選択", + "images": "画像", + "assetsTab": "プロジェクトで使用するためにアップロードされたファイル。", + "imagesTab": "Invoke内であなたが作成および保存した画像。", + "assets": "アセット", + "useForPromptGeneration": "プロンプト生成に使用する", + "jump": "ジャンプ", + "noImagesInGallery": "表示する画像がありません", + "unableToLoad": "ギャラリーを読み込めません", + "selectAnImageToCompare": "比較する画像を選択", + "openViewer": "ビューアを開く", + "closeViewer": "ビューアを閉じる", + "usePagedGalleryView": "ページ型ギャラリービュー", + "loadingGallery": "ギャラリーをロード中...", + "loadingMetadata": "メタデータをロード中...", + "noImagesFound": "画像が見つかりません", + "bulkDownloadReady": "ダウンロード準備完了", + "clickToDownload": "クリックしてダウンロード" + }, + "hotkeys": { + "searchHotkeys": "ショートカットキーを検索", + "clearSearch": "検索をクリア", + "noHotkeysFound": "ショートカットキーが見つかりません", + "viewer": { + "runPostprocessing": { + "title": "ポストプロセスを実行", + "desc": "現画像の選択された後処理を実行." + }, + "useSize": { + "title": "サイズを使用", + "desc": "現画像のサイズをバウンディングボックスのサイズとして使用する." + }, + "recallPrompts": { + "title": "プロンプトを再使用", + "desc": "現画像のポジティブプロンプトとネガティブプロンプトを呼び出す." + }, + "recallAll": { + "title": "全てのメタデータを再使用", + "desc": "現画像の全てのメタデータを呼び出す." + }, + "recallSeed": { + "title": "シード値を再使用", + "desc": "現画像の全てのシードを呼び出す." + }, + "swapImages": { + "desc": "比較されている画像を交換.", + "title": "比較画像を交換" + }, + "nextComparisonMode": { + "title": "次の比較モード", + "desc": "比較モード切り替え." + }, + "toggleMetadata": { + "desc": "現画像のメタデータオーバーレイを表示/非表示.", + "title": "メタデータの表示/非表示" + }, + "loadWorkflow": { + "title": "ワークフロー読み込み", + "desc": "現在の画像で保存されたワークフローを読み込み(1つ持っていたら)." + }, + "title": "画像ビューア", + "toggleViewer": { + "title": "画像ビューアの表示/非表示", + "desc": "画像ビューアを表示/非表示.キャンバスタブだけで利用可能." + }, + "remix": { + "desc": "現画像のシードを除き,全てのメタデータを呼び出す.", + "title": "リミックス" + } + }, + "canvas": { + "redo": { + "title": "やり直し", + "desc": "最後のキャンバス操作をやり直します。" + }, + "transformSelected": { + "title": "変形", + "desc": "選択したレイヤーを変形します。" + }, + "undo": { + "title": "取り消し", + "desc": "最後のキャンバス操作を取り消します。" + }, + "selectEraserTool": { + "title": "消しゴムツール", + "desc": "消しゴムツールを選択します。" + }, + "cancelTransform": { + "title": "変形をキャンセル", + "desc": "保留中の変形をキャンセルします。" + }, + "resetSelected": { + "title": "レイヤーをリセット", + "desc": "選択したレイヤーをリセットします。この操作はInpaint MaskおよびRegional Guidanceにのみ適用されます。" + }, + "applyTransform": { + "title": "変形を適用", + "desc": "保留中の変形を選択したレイヤーに適用します。" + }, + "selectColorPickerTool": { + "title": "スポイトツール", + "desc": "スポイトツールを選択します。" + }, + "fitBboxToCanvas": { + "title": "バウンディングボックスをキャンバスにフィット", + "desc": "バウンディングボックスがキャンバスに収まるように表示を拡大、位置調整します。" + }, + "selectBrushTool": { + "title": "ブラシツール", + "desc": "ブラシツールを選択します。" + }, + "selectMoveTool": { + "title": "移動ツール", + "desc": "移動ツールを選択します。" + }, + "selectBboxTool": { + "title": "バウンディングボックスツール", + "desc": "バウンディングボックスツールを選択します。" + }, + "title": "キャンバス", + "fitLayersToCanvas": { + "title": "キャンバスに表示レイヤーをフィット", + "desc": "すべての表示レイヤーがキャンバスに収まるように表示を拡大、位置調整します。" + }, + "setZoomTo400Percent": { + "desc": "キャンバスのズームを400%に設定します。", + "title": "400%にズーム" + }, + "setZoomTo800Percent": { + "title": "800%にズーム", + "desc": "キャンバスのズームを800%に設定します。" + }, + "quickSwitch": { + "title": "レイヤーのクイックスイッチ", + "desc": "最後に選択した2つのレイヤー間を切り替えます。レイヤーがブックマークされている場合、常にそのレイヤーと最後に選択したブックマークされていないレイヤーの間を切り替えます。" + }, + "nextEntity": { + "title": "次のレイヤー", + "desc": "リスト内の次のレイヤーを選択します。" + }, + "filterSelected": { + "title": "フィルター", + "desc": "選択したレイヤーをフィルターします。RasterおよびControlレイヤーにのみ適用されます。" + }, + "prevEntity": { + "desc": "リスト内の前のレイヤーを選択します。", + "title": "前のレイヤー" + }, + "selectViewTool": { + "title": "表示ツール", + "desc": "表示ツールを選択します。" + }, + "setZoomTo100Percent": { + "title": "100%にズーム", + "desc": "キャンバスのズームを100%に設定します。" + }, + "deleteSelected": { + "desc": "選択したレイヤーを削除します。", + "title": "レイヤーを削除" + }, + "cancelFilter": { + "desc": "保留中のフィルターをキャンセルします。", + "title": "フィルターをキャンセル" + }, + "applyFilter": { + "title": "フィルターを適用", + "desc": "保留中のフィルターを選択したレイヤーに適用します。" + }, + "setZoomTo200Percent": { + "title": "200%にズーム", + "desc": "キャンバスのズームを200%に設定します。" + }, + "decrementToolWidth": { + "title": "ツール幅を縮小する", + "desc": "選択中のブラシまたは消しゴムツールの幅を減少させます。" + }, + "incrementToolWidth": { + "desc": "選択中のブラシまたは消しゴムツールの幅を増加させます。", + "title": "ツール幅を増加する" + }, + "selectRectTool": { + "title": "シェイプツール", + "desc": "シェイプツールを選択します。" + }, + "settings": { + "behavior": "挙動", + "display": "表示", + "grid": "グリッド", + "debug": "デバッグ" + }, + "toggleNonRasterLayers": { + "title": "非ラスターレイヤーの切り替え", + "desc": "ラスター以外のレイヤー カテゴリ (コントロール レイヤー、インペイント マスク、領域ガイダンス) を表示または非表示にします。" + }, + "setFillColorsToDefault": { + "title": "デフォルトカラーをセット", + "desc": "現在のツールの色をデフォルトに設定します。" + }, + "toggleFillColor": { + "title": "メインカラーとサブカラーの切り替え", + "desc": "現在のツールのメインカラーとサブカラーを切り替えます。" + }, + "invertMask": { + "title": "マスクを変換", + "desc": "選択したインペイント マスクを反転し、反対の透明度を持つ新しいマスクを作成します。" + }, + "fitBboxToLayers": { + "title": "バウンディングボックスを表示レイヤーにフィット", + "desc": "表示されているレイヤーに合わせて生成バウンディングボックスを自動的に調整します" + }, + "fitBboxToMasks": { + "title": "バウンディングボックスをマスクにフィットさせる", + "desc": "可視のインペイントマスクに合わせて生成バウンディングボックスを自動的に調整します" + }, + "toggleBbox": { + "title": "バウンディングボックスの表示/非表示を切り替える", + "desc": "生成バウンディングボックスを非表示または表示する" + }, + "applySegmentAnything": { + "title": "Segment Anythingを適用する", + "desc": "現在のSegment Anythingマスクを適用します。", + "key": "入力" + }, + "cancelSegmentAnything": { + "title": "セグメントをキャンセル", + "desc": "現在のSegment Anything操作をキャンセルします。", + "key": "エスケープ" + }, + "selectLassoTool": { + "title": "投げ縄ツール", + "desc": "投げ縄ツールを選択します。" + }, + "mergeDown": { + "title": "下のレイヤーと結合", + "desc": "選択したレイヤーを直下のレイヤーと結合します。" + }, + "mergeVisible": { + "title": "表示レイヤーを結合", + "desc": "選択中の種別の表示レイヤーをマージします。" + } + }, + "workflows": { + "undo": { + "title": "取り消し", + "desc": "直前のワークフローアクションを元に戻す." + }, + "redo": { + "title": "やり直し", + "desc": "直前のワークフローアクションをやり直す." + }, + "title": "ワークフロー", + "pasteSelection": { + "title": "ペースト", + "desc": "コピーしたノードとエッジを貼り付けます." + }, + "copySelection": { + "title": "コピー", + "desc": "選択したノードとエッジをコピーします." + }, + "deleteSelection": { + "title": "削除", + "desc": "選択されたノードとエッジを削除." + }, + "selectAll": { + "desc": "全てのノードとエッジを選択.", + "title": "全選択" + }, + "addNode": { + "desc": "ノード追加メニューを開く。", + "title": "ノードを追加" + }, + "pasteSelectionWithEdges": { + "title": "エッジ付き貼り付け", + "desc": "コピーされたノード,エッジ,コピーされたノードに付属した全てのエッジを貼り付けます." + } + }, + "app": { + "toggleLeftPanel": { + "title": "左パネルをトグル", + "desc": "左パネルを表示または非表示。" + }, + "title": "アプリケーション", + "invoke": { + "title": "生成", + "desc": "生成をキューに追加し、キューの末尾に加えます。" + }, + "cancelQueueItem": { + "title": "キャンセル", + "desc": "現在処理中のキュー項目をキャンセルします。" + }, + "clearQueue": { + "title": "キューをクリア", + "desc": "すべてのキュー項目をキャンセルして消去します。" + }, + "selectCanvasTab": { + "desc": "キャンバスタブを選択します。", + "title": "キャンバスタブを選択" + }, + "selectUpscalingTab": { + "desc": "アップスケールタブを選択します。", + "title": "アップスケールタブを選択" + }, + "toggleRightPanel": { + "desc": "右パネルを表示または非表示。", + "title": "右パネルをトグル" + }, + "selectModelsTab": { + "title": "モデルタブを選択", + "desc": "モデルタブを選択します。" + }, + "invokeFront": { + "desc": "生成をキューに追加し、キューの先頭に加えます。", + "title": "生成(先頭)" + }, + "resetPanelLayout": { + "title": "パネルレイアウトをリセット", + "desc": "左パネルと右パネルをデフォルトのサイズとレイアウトにリセットします。" + }, + "togglePanels": { + "desc": "左パネルと右パネルを合わせて表示または非表示。", + "title": "パネルをトグル" + }, + "selectWorkflowsTab": { + "desc": "ワークフロータブを選択します。", + "title": "ワークフロータブを選択" + }, + "selectQueueTab": { + "title": "キュータブを選択", + "desc": "キュータブを選択します。" + }, + "focusPrompt": { + "title": "プロンプトにフォーカス", + "desc": "カーソルをポジティブプロンプト欄に移動します。" + }, + "promptHistoryPrev": { + "title": "ヒストリー内の前のプロンプト", + "desc": "プロンプトがフォーカスされている場合、ヒストリー内の前の(古い)プロンプトに移動します。" + }, + "promptHistoryNext": { + "title": "ヒストリーの次のプロンプト", + "desc": "プロンプトがフォーカスされている場合、ヒストリーの次の(新しい)プロンプトに移動します。" + }, + "selectGenerateTab": { + "title": "生成タブを選択", + "desc": "生成タブを選択。", + "key": "1" + }, + "promptWeightUp": { + "title": "選択したプロンプトの重みを増加", + "desc": "プロンプトのテキストが選択されている際に、選択されているプロンプトの重みを増やします。" + }, + "promptWeightDown": { + "title": "選択されているプロンプトの重みを減らす", + "desc": "プロンプトのテキストが選択されている際に、選択されているプロンプトの重みを減らします。" + } + }, + "hotkeys": "ショートカットキー", + "gallery": { + "title": "ギャラリー", + "galleryNavLeftAlt": { + "title": "左へ移動(画像比較)", + "desc": "左へ移動と同じですが,比較画像を選択し,比較モードがまだ開かれていなければ開きます." + }, + "galleryNavUp": { + "desc": "ギャラリーグリッド内を上に移動し,その画像を選択.ページの上部にある場合,前のページに移動.", + "title": "上へ移動" + }, + "galleryNavDown": { + "title": "下へ移動", + "desc": "ギャラリーグリッド内の下に移動し,その画像を選択.ページの下にある場合,次のページに移動." + }, + "clearSelection": { + "title": "選択を消去", + "desc": "選択していれば消去." + }, + "deleteSelection": { + "desc": "選択した全ての画像を削除します.デフォルトでは,削除について確認されます.アプリ内で画像を利用中の場合,警告されます.", + "title": "削除" + }, + "galleryNavDownAlt": { + "desc": "下へ移動と同じですが,比較画像を選択し,比較モードがまだ開かれていなければ開きます.", + "title": "下へ移動(画像比較)" + }, + "galleryNavUpAlt": { + "title": "上へ移動(画像比較)", + "desc": "上へ移動と同じですが,比較画像を選択し,比較モードがまだ開かれていなければ開きます." + }, + "selectAllOnPage": { + "desc": "現在のページ上の全ての画像を選択.", + "title": "ページ上で全てを選択" + }, + "galleryNavRight": { + "title": "右へ移動", + "desc": "ギャラリーグリッド内を右に移動し,その画像を選択.行の最後の画像にある場合,次の行に移動.ページの最後の画像にある場合,次のページに移動." + }, + "galleryNavRightAlt": { + "title": "右へ移動(画像比較)", + "desc": "右へ移動と同じですが,比較画像を選択し,比較モードがまだ開かれていなければ開きます." + }, + "galleryNavLeft": { + "desc": "ギャラリーグリッド内を左に移動し.その画像を選択.行の最初の画像にある場合,前の行に移動.ページの最初の画像にある場合,前のページに移動.", + "title": "左へ移動" + }, + "starImage": { + "title": "画像にスターを付ける/スターを外す", + "desc": "選択した画像にスターを付けたり、スターを外したりします。" + } + }, + "editMode": "編集モード", + "viewMode": "ビューモード", + "editHotkey": "ショートカットキーの編集", + "addHotkey": "ショートカットキーの追加", + "resetToDefault": "デフォルトにリセット", + "resetAll": "全てをデフォルトにリセット", + "resetAllConfirmation": "すべてのショートカットキーをデフォルトに戻してよろしいですか?この操作は取り消せません。", + "enterHotkeys": "カンマ区切りでショートカットキーを入力してください", + "save": "保存", + "cancel": "キャンセル", + "modifiers": "モディファイア", + "syntaxHelp": "構文のヘルプ", + "multipleHotkeys": "カンマで区切られた複数のショートカットキー", + "help": "ヘルプ", + "noHotkeysRecorded": "まだショートカットキーが記録されていません", + "pressKeys": "キーを押してください...", + "setHotkey": "セット", + "setAnother": "他をセット", + "removeLastHotkey": "最後のショートカットキーを削除", + "clearAll": "全てをクリア", + "duplicateWarning": "このショートカットキーはすでに記録済みです", + "conflictWarning": "はすでに \"{{hotkeyTitle}}\" で使われています", + "thisHotkey": "このショートカットキー", + "combineWith": "組み合わせ +", + "validKeys": "有効なキー" + }, + "modelManager": { + "modelManager": "モデルマネージャ", + "model": "モデル", + "allModels": "すべてのモデル", + "modelUpdated": "モデルをアップデート", + "manual": "手動", + "name": "名前", + "description": "概要", + "config": "コンフィグ", + "repo_id": "リポジトリID", + "width": "幅", + "height": "高さ", + "addModel": "モデルを追加", + "availableModels": "モデルを有効化", + "search": "検索", + "load": "ロード", + "active": "アクティブ", + "selected": "選択済", + "delete": "削除", + "deleteModel": "モデルを削除", + "deleteConfig": "設定を削除", + "deleteMsg1": "InvokeAIからこのモデルを削除してよろしいですか?", + "deleteMsg2": "これは、モデルがInvokeAIルートフォルダ内にある場合、ディスクからモデルを削除します。カスタム保存場所を使用している場合、モデルはディスクから削除されません。", + "none": "なし", + "convert": "変換", + "convertToDiffusersHelpText6": "このモデルを変換しますか?", + "settings": "設定", + "convertingModelBegin": "モデルを変換しています...", + "baseModel": "ベースモデル", + "modelDeleteFailed": "モデルの削除ができませんでした", + "convertToDiffusers": "ディフューザーに変換", + "alpha": "アルファ", + "modelConverted": "モデル変換が完了しました", + "predictionType": "予測タイプ(SD 2.x モデルおよび一部のSD 1.x モデル用)", + "selectModel": "モデルを選択", + "advanced": "高度", + "modelDeleted": "モデルが削除されました", + "convertToDiffusersHelpText2": "このプロセスでは、モデルマネージャーのエントリーを同じモデルのディフューザーバージョンに置き換えます。", + "modelUpdateFailed": "モデル更新が失敗しました", + "convertToDiffusersHelpText5": "十分なディスク空き容量があることを確認してください。モデルは一般的に2GBから7GBのサイズがあります。", + "modelConversionFailed": "モデル変換が失敗しました", + "syncModels": "モデルを同期", + "modelType": "モデルタイプ", + "convertToDiffusersHelpText1": "このモデルは 🧨 Diffusers フォーマットに変換されます。", + "convertToDiffusersHelpText3": "チェックポイントファイルは、InvokeAIルートフォルダ内にある場合、ディスクから削除されます。カスタムロケーションにある場合は、削除されません。", + "convertToDiffusersHelpText4": "これは一回限りのプロセスです。コンピュータの仕様によっては、約30秒から60秒かかる可能性があります。", + "cancel": "キャンセル", + "uploadImage": "画像をアップロード", + "addModels": "モデルを追加", + "modelName": "モデル名", + "source": "ソース", + "path": "パス", + "modelSettings": "モデル設定", + "vae": "VAE", + "huggingFace": "HuggingFace", + "huggingFaceRepoID": "HuggingFace リポジトリID", + "metadata": "メタデータ", + "loraModels": "LoRA", + "edit": "編集", + "install": "インストール", + "huggingFacePlaceholder": "owner/model-name", + "variant": "Variant", + "scanFolderHelper": "フォルダはモデル向けに再起的にスキャンされます.これは大きいフォルダほど多少の時間がかかります.", + "controlLora": "コントロールLoRA", + "triggerPhrases": "トリガーワード", + "t5Encoder": "T5エンコーダー", + "textualInversions": "Textual Inversions", + "fluxRedux": "FLUX リダックス", + "installQueue": "インストール進捗状況", + "noMatchingModels": "一致するモデルがありません", + "noDefaultSettings": "このモデルにはデフォルト設定が構成されていません。デフォルト設定を追加するためにはモデルマネージャーにアクセスしてください。", + "usingDefaultSettings": "モデルのデフォルト設定を使用する", + "defaultSettingsOutOfSync": "いくつかの設定がデフォルト設定とマッチしません:", + "vaePrecision": "VAE精度", + "installingBundle": "バンドルをインストール", + "urlOrLocalPathHelper": "URLは1つのファイルを示さなくてはいけません.ローカルパスは1つのファイルか,1つのディヒューザーモデルのあるフォルダを指定できます.", + "clipEmbed": "クリップ埋め込み", + "loraTriggerPhrases": "LoRAトリガーワード", + "main": "メイン", + "defaultSettings": "デフォルト設定", + "deleteModelImage": "モデル画像を削除", + "hfTokenInvalid": "HuggingFaceトークンが無効または見つかりません", + "hfForbiddenErrorMessage": "リポジトリにアクセスすることを勧めます.所有者はダウンロードにあたり利用規約への同意を要求する場合があります.", + "noModelsInstalled": "インストールされているモデルがありません", + "pathToConfig": "設定ファイルパス", + "noModelsInstalledDesc1": "モデルを一緒にインストール", + "pruneTooltip": "完了したインポートをキューから削除", + "scanResults": "結果をスキャン", + "scanPlaceholder": "ローカルフォルダへのパス", + "typePhraseHere": "ここにフレーズを入力", + "modelImageUpdated": "モデル画像がアップデートされました", + "installAll": "全てインストール", + "installRepo": "リポジトリをインストール", + "localOnly": "ローカルのみ", + "huggingFaceHelper": "いくつかのモデルがこのリポジトリで見つかった場合,1つを選択してインストールするように求められます.", + "hfTokenInvalidErrorMessage": "HuggingFaceトークンが無効または見つかりません。", + "hfTokenRequired": "有効なHuggingFaceトークンが必要なモデルをダウンロードしようとしています。", + "hfTokenInvalidErrorMessage2": "更新してください ", + "modelImageDeleted": "モデル画像が削除されました", + "repoVariant": "リポジトリバリアント", + "llavaOnevision": "LLaVA ワンビジョン", + "installingXModels_other": "{{count}} 個のモデルをインストール", + "skippingXDuplicates_other": "{{count}}個の重複をスキップ", + "clipGEmbed": "クリップ-G 埋め込み", + "mainModelTriggerPhrases": "メインモデルトリガーワード", + "upcastAttention": "アップキャストアテンション", + "urlOrLocalPath": "URLかローカルパス", + "clipLEmbed": "クリップ-L 埋め込み", + "defaultSettingsSaved": "デフォルト設定を保存しました", + "hfTokenUnableToVerify": "HuggingFaceトークンを確認できません", + "hfForbidden": "このHuggingFaceモデルにアクセスできません", + "hfTokenLabel": "HuggingFaceトークン(いくつかのモデルに必要)", + "noModelSelected": "モデルが選択されていません", + "prune": "除去", + "hfTokenHelperText": "いくつかのモデルにHuggingFaceトークンが必要です。ここをクリックしてあなたのトークンを作成してください。", + "starterBundleHelpText": "メインモデル,コントロールネット,IPアダプターなど,ベースモデルから始めるのに必要なすべてのモデルを簡単にインストールできます.バンドルを選択すると,すでにインストールされているモデルはスキップされます.", + "inplaceInstallDesc": "ファイルを移動せずにモデルをインストールします。このモデルを利用する際には元の場所からロードされます。これが利用できない場合、モデルファイルはInvoke管理下のモデルディレクトリに移動されインストールされます。", + "hfTokenUnableToVerifyErrorMessage": "HuggingFaceトークンを確認できません。ネットワークによるエラーの可能性があります。後ほどトライしてください。", + "restoreDefaultSettings": "クリックするとモデルのデフォルト設定が使用されます.", + "hfTokenSaved": "HuggingFaceトークンを保存しました", + "imageEncoderModelId": "画像エンコーダーモデルID", + "includesNModels": "{{n}}個のモデルとこれらの依存関係を含みます。", + "learnMoreAboutSupportedModels": "サポートされているモデルについて更に学ぶ", + "modelImageUpdateFailed": "モデル画像のアップデートに失敗しました", + "scanFolder": "スキャンフォルダ", + "simpleModelPlaceholder": "ローカルファイルかディフューザーズフォルダへのURLかパス", + "installingModel": "モデルをインストール", + "sigLip": "シグリップ", + "spandrelImageToImage": "Image to Image(スパンドレル)", + "starterBundles": "スターターバンドル", + "starterModels": "スターターモデル", + "modelImageDeleteFailed": "モデル画像の削除に失敗しました", + "urlForbidden": "このモデルにアクセスできません", + "urlForbiddenErrorMessage": "このモデルを配布しているサイトからリクエスト権限が必要かもしれません.", + "urlUnauthorizedErrorMessage": "このモデルにアクセスするためにAPIトークンを構成する必要があるかもしれません.", + "urlUnauthorizedErrorMessage2": "ここでどうやるか学びます.", + "inplaceInstall": "元位置でインストール", + "fileSize": "ファイルサイズ", + "modelPickerFallbackNoModelsInstalled2": "モデルマネージャー にアクセスしてモデルをインストールしてください.", + "modelPickerFallbackNoModelsInstalled": "モデルがインストールされていません.", + "manageModels": "モデル管理", + "hfTokenReset": "HuggingFaceトークンをリセット", + "relatedModels": "関連のあるモデル", + "installedModelsCount": "{{total}} モデルのうち {{installed}} 個がインストールされています。", + "allNModelsInstalled": "{{count}} 個のモデルがすべてインストールされています", + "nToInstall": "{{count}}個をインストールする", + "nAlreadyInstalled": "{{count}} 個すでにインストールされています", + "bundleAlreadyInstalled": "バンドルがすでにインストールされています", + "bundleAlreadyInstalledDesc": "{{bundleName}} バンドル内のすべてのモデルはすでにインストールされています。", + "launchpadTab": "ローンチパッド", + "launchpad": { + "welcome": "モデルマネージャーへようこそ", + "description": "Invokeの多様な機能を利用するには、各種モデルのインストールが必要です。手動インストールオプションから選択するか、厳選されたスターターモデルをご覧ください。", + "manualInstall": "マニュアルインストール", + "urlDescription": "URLまたはローカルファイルパスからモデルをインストールします。特定のモデルを追加したい場合に最適です。", + "huggingFaceDescription": "HuggingFace リポジトリからモデルを直接参照してインストールします。", + "scanFolderDescription": "ローカルフォルダをスキャンしてモデルを自動的に検出し、インストールします。", + "recommendedModels": "推奨モデル", + "exploreStarter": "または、利用可能なすべてのスターターモデルを参照してください", + "bundleDescription": "各バンドルにはそれぞれのモデルファミリー向けの必須モデルと、厳選されたベースモデルが含まれています。", + "sdxl": "SDXL", + "quickStart": "クイックスタートバンドル", + "browseAll": "利用可能なすべてのモデルを見る:", + "stableDiffusion15": "Stable Diffusion 1.5", + "fluxDev": "FLUX.1 dev", + "externalDescription": "GeminiやOpenAIなどの外部画像生成がAPIキー経由で利用できます。利用コストはプロバイダー側で発生します。" + }, + "filterModels": "フィルターモデル", + "installBundle": "バンドルをインストール", + "installBundleMsg1": "{{bundleName}} バンドルをインストールしてもよろしいですか?", + "installBundleMsg2": "このバンドルでは、次の {{count}} モデルがインストールされます:", + "ipAdapters": "IPアダプター", + "showOnlyRelatedModels": "関連している", + "starterModelsInModelManager": "スターターモデルはモデルマネージャーにあります", + "actions": "一括操作", + "selectAll": "全て選択", + "deselectAll": "全て選択解除", + "deleteModelsConfirm": "本当に {{count}} 個のモデルを削除しますか? このアクションは取り消せません。", + "deleteWarning": "Invokeのモデルディレクトリにあるモデルは、ディスクから完全に削除されます。", + "modelsDeleted": "{{count}} 個のモデルの削除に成功しました", + "modelsDeleteFailed": "モデルの削除に失敗しました", + "someModelsFailedToDelete": "{{count}}個のモデルを削除できませんでした", + "modelsDeletedPartial": "一部完了", + "someModelsDeleted": "{{deleted}} を削除, {{failed}} が失敗", + "modelsDeleteError": "モデルの削除中にエラーが発生しました", + "pause": "一時停止", + "pauseAll": "全て一時停止", + "pauseAllTooltip": "アクティブなダウンロードを全て一時停止", + "resume": "再開", + "resumeAll": "全て再開", + "resumeAllTooltip": "一時停止したダウンロードを全て再開", + "restartFailed": "再開に失敗しました", + "restartFile": "ファイルを再開", + "restartRequired": "リスタートが必要です", + "resumeRefused": "再開がサーバーに拒否されました。再開が必要です。", + "backendDisconnected": "バックエンドとの接続が切れました", + "cancelAll": "すべてキャンセル", + "cancelAllTooltip": "アクティブなダウンロードを全てキャンセル", + "reidentify": "再識別", + "reidentifyTooltip": "モデルが正しくインストールされなかった場合(例:タイプが間違っている、動作しない場合)、モデルの再識別を実行してみてください。ただし、編集したカスタム設定はすべてリセットされます。", + "reidentifySuccess": "モデルの再識別に成功", + "reidentifyUnknown": "モデルの識別ができません", + "reidentifyError": "モデルの識別中にエラーが発生", + "reidentifyModels": "モデルの再識別", + "reidentifyModelsConfirm": "{{count}} 個のモデルを再識別しますか? モデルが再度解析され、正しい形式と設定が特定されます。", + "reidentifyWarning": "これらのモデルに適用したカスタム設定はすべてリセットされます。", + "modelsReidentified": "{{count}} 個のモデルの再識別に成功", + "modelsReidentifyFailed": "モデルの再識別に失敗", + "someModelsFailedToReidentify": "{{count}} このモデルを再識別できませんでした", + "modelsReidentifiedPartial": "一部完了", + "someModelsReidentified": "{{succeeded}} 再識別完了, {{failed}} 失敗", + "modelsReidentifyError": "モデルの再識別中にエラー", + "updatePath": "パスを更新", + "updatePathTooltip": "モデルファイルを新しい場所に移動した場合は、このモデルのファイルパスを更新してください。", + "updatePathDescription": "モデルファイルまたはディレクトリへの新しいパスを入力してください。モデルファイルをディスク上で手動で移動した場合に使用してください。", + "currentPath": "現在のパス", + "newPath": "新しいパス", + "newPathPlaceholder": "新しいパスを入力...", + "pathUpdated": "モデルのパス更新に成功しました", + "pathUpdateFailed": "モデルのパス更新に失敗しました", + "invalidPathFormat": "パスは絶対パスである必要があります (例 C:\\Models\\... or /home/user/models/...)", + "cpuOnly": "CPUのみ", + "runOnCpu": "テキストエンコーダーモデルをCPUのみで実行", + "deleteModels": "モデルを削除", + "unidentifiedModelTitle": "モデルの識別ができません", + "unidentifiedModelMessage": "インストールされているモデルの種類、ベース、および/またはフォーマットを特定できませんでした。モデルを編集して、モデルに適した設定を選択してください。", + "unidentifiedModelMessage2": "正しい設定が表示されない場合、または設定を変更してもモデルが動作しない場合は、でヘルプを求めるか、で問題を報告してください。", + "missingFiles": "見つからないファイル", + "missingFilesTooltip": "ディスクにモデルファイルが見つかりません", + "modelFormat": "モデルフォーマット", + "modelSettingsWarning": "これらの設定は、Invokeにモデルの種類と読み込み方法を指示します。モデルのインストール時にInvokeがこれらの設定を正しく検出できなかった場合、またはモデルが「不明」と分類されている場合は、手動で編集する必要があるかもしれません。", + "modelPickerFallbackNoModelsInstalledNonAdmin": "モデルがインストールされていません。InvokeAI管理者()にモデルのインストールを依頼してください。", + "noModelsInstalledAskAdmin": "管理者にインストールを依頼してください。", + "syncModelsTooltip": "InvokeAIのルートディレクトリにある未使用のモデルファイルを特定し、削除します。", + "syncModelsDirectory": "モデルディレクトリを同期する", + "externalProviders": "外部プロバイダー", + "externalSetupTitle": "外部プロバイダーの設定", + "externalSetupDescription": "API キーを設定し、外部プロバイダーによる画像生成を有効にします。", + "externalImageGenerator": "外部画像生成サービス", + "externalInstallDefaults": "スターターモデルの自動インストール", + "externalProvidersUnavailable": "このビルドでは外部プロバイダーは利用できません。", + "externalSetupFooter": "APIキーが必要です。外部プロバイダーのAPIを使用する場合、利用コストはプロバイダー側で発生します。", + "externalProviderCardDescription": "外部画像生成サービス {{providerId}} の認証情報を設定します。", + "externalApiKey": "APIキー", + "externalApiKeyPlaceholder": "APIキーを貼り付け", + "externalApiKeyPlaceholderSet": "APIキー設定済み", + "externalApiKeyHelper": "InvokeAIルートディレクトリのapi_keys.yaml に保存されます。", + "externalBaseUrl": "ベースURL(オプション)", + "externalBaseUrlHelper": "必要に応じてデフォルト API のURL を上書きしてください。", + "externalResetHelper": "APIキーとベースURLをクリアします。", + "sortByName": "名前", + "sortByBase": "ベースモデル", + "sortBySize": "サイズ", + "sortByDateAdded": "追加日", + "sortByDateModified": "変更日", + "sortByPath": "パス", + "sortByType": "タイプ", + "sortByFormat": "フォーマット", + "sortDefault": "デフォルト", + "externalProvider": "外部プロバイダ", + "externalCapabilities": "外部利用可能機能", + "externalDefaults": "外部デフォルト設定", + "providerId": "プロバイダID", + "providerModelId": "プロバイダモデルID", + "supportedModes": "サポートされているモード", + "supportsNegativePrompt": "ネガティブプロンプトのサポート", + "supportsReferenceImages": "参照画像のサポート", + "supportsSeed": "シード値のサポート", + "supportsGuidance": "ガイド画像のサポート", + "maxImagesPerRequest": "リクエストあたりの最大画像数", + "maxReferenceImages": "参照画像の最大数", + "maxImageWidth": "画像の最大横幅", + "sourceUrl": "ソースURL", + "textLLM": "LLM", + "filesCount": "{{count}} ファイル", + "deleteSelected": "選択中の {{count}} を削除", + "orphanedModelsDeleteErrors": "いくつかのモデルが削除できませんでした", + "queueEmpty": "インストールキューが空です。", + "selectModelToView": "モデルを選択して詳細を表示", + "importSettings": "設定をインポート", + "exportSettings": "設定をエクスポート", + "settingsExported": "モデル設定がエクスポートされました", + "settingsImported": "モデル設定がインポートされました", + "settingsImportedPartial": "モデル設定は部分的にインポートされました。互換性のない設定はスキップされました: {{fields}}", + "settingsImportFailed": "モデル設定のインポートに失敗しました", + "settingsImportIncompatible": "設定ファイルにこのモデルタイプと互換性のある設定項目がありません", + "settingsImportInvalidFile": "不正な設定ファイルです" + }, + "parameters": { + "images": "画像", + "steps": "ステップ", + "width": "幅", + "height": "高さ", + "seed": "シード値", + "shuffle": "シャッフル", + "strength": "強度", + "upscaling": "アップスケール", + "scale": "スケール", + "scaleBeforeProcessing": "生成前のリサイズ", + "scaledWidth": "幅のスケール", + "scaledHeight": "高さのスケール", + "usePrompt": "プロンプトを使用", + "useSeed": "シード値を使用", + "useAll": "すべてを使用", + "info": "情報", + "iterations": "生成回数", + "general": "基本設定", + "setToOptimalSize": "サイズをモデルに最適化", + "invoke": { + "addingImagesTo": "画像の追加先", + "collectionTooFewItems": "少なすぎる項目,最小{{minItems}}", + "collectionTooManyItems": "多すぎる項目,最大{{maxItems}}", + "missingFieldTemplate": "フィールドテンプレートの欠落", + "collectionStringTooShort": "短すぎます,最小{{minLength}}", + "batchNodeEmptyCollection": "いくつかのバッチノードに空のコレクションがあります", + "collectionNumberLTExclusiveMin": "{{value}} <= {{exclusiveMinimum}} (最小値を除く)", + "missingInputForField": "入力の欠落", + "noModelSelected": "モデルが選択されていません", + "collectionStringTooLong": "長すぎます,最大{{maxLength}}", + "batchNodeCollectionSizeMismatchNoGroupId": "バッチグループのコレクションサイズが合いません", + "invoke": "呼び出す", + "collectionEmpty": "空のコレクション", + "collectionNumberGTExclusiveMax": "{{value}} >= {{exclusiveMaximum}} (最大値を除く)", + "collectionNumberNotMultipleOf": "{{value}} は {{multipleOf}} の倍数ではありません", + "batchNodeCollectionSizeMismatch": "バッチ {{batchGroupId}}のコレクションサイズが合いません", + "collectionNumberGTMax": "{{value}} > {{maximum}} (最大増加)", + "missingNodeTemplate": "ノードテンプレートの欠落", + "batchNodeNotConnected": "バッチノードが: {{label}}につながっていない", + "collectionNumberLTMin": "{{value}} < {{minimum}} (最小増加)", + "fluxModelMultipleControlLoRAs": "コントロールLoRAは1度に1つしか使用できません", + "noPrompts": "プロンプトが生成されません", + "noNodesInGraph": "グラフにノードがありません", + "noCLIPEmbedModelSelected": "FLUX生成にCLIPエンベッドモデルが選択されていません", + "canvasIsFiltering": "キャンバスがビジー状態(フィルタリング)", + "canvasIsCompositing": "キャンバスがビジー状態(合成)", + "systemDisconnected": "システムが切断されました", + "canvasIsTransforming": "キャンバスがビジー状態(変換)", + "canvasIsRasterizing": "キャンバスがビジー状態(ラスタライズ)", + "modelIncompatibleBboxHeight": "バウンディングボックスの高さは{{height}}ですが,{{model}}は{{multiple}}の倍数が必要です", + "modelIncompatibleScaledBboxHeight": "バウンディングボックスの高さは{{height}}ですが,{{model}}は{{multiple}}の倍数を必要です", + "modelIncompatibleBboxWidth": "バウンディングボックスの幅は{{width}}ですが, {{model}}は{{multiple}}の倍数が必要です", + "modelIncompatibleScaledBboxWidth": "バウンディングボックスの幅は{{width}}ですが,{{model}}は{{multiple}}の倍数が必要です", + "canvasIsSelectingObject": "キャンバスがビジー状態(オブジェクトの選択)", + "noFLUXVAEModelSelected": "FLUX生成にVAEモデルが選択されていません", + "noT5EncoderModelSelected": "FLUX生成にT5エンコーダモデルが選択されていません", + "modelDisabledForTrial": "{{modelName}} を使用した生成はトライアルアカウントではご利用いただけません.アカウント設定にアクセスしてアップグレードしてください。", + "promptExpansionPending": "プロンプト拡張が進行中", + "promptExpansionResultPending": "プロンプト拡張結果を受け入れるか破棄してください", + "emptyBatches": "空のバッチ", + "noStartingFrameImage": "開始フレーム画像がありません", + "fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16)、バウンディングボックスの幅は{{width}}です", + "fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16)、バウンディングボックスの高さは{{height}}です", + "fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16)、リサイズ後のバウンディングボックスの幅は{{width}}です", + "fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16)、リサイズ後のバウンディングボックスの高さは {{height}} です", + "incompatibleLoRAs": "互換性のない LoRA が追加されました" + }, + "aspect": "縦横比", + "lockAspectRatio": "縦横比を固定", + "scheduler": "スケジューラ", + "sendToUpscale": "アップスケーラーに転送", + "useSize": "サイズを使用", + "postProcessing": "ポストプロセス (Shift + U)", + "denoisingStrength": "ノイズ強度", + "recallMetadata": "メタデータを再使用", + "copyImage": "画像をコピー", + "positivePromptPlaceholder": "ポジティブプロンプト", + "negativePromptPlaceholder": "ネガティブプロンプト", + "type": "タイプ", + "cancel": { + "cancel": "キャンセル" + }, + "cfgScale": "CFGスケール", + "tileSize": "タイルサイズ", + "coherenceMode": "モード", + "disabledNoRasterContent": "無効(ラスターコンテンツなし)", + "imageFit": "初期画像を出力サイズに合わせる", + "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (おそらく大きすぎます)", + "coherenceEdgeSize": "境界の拡張", + "swapDimensions": "縦横サイズを入れ替え", + "controlNetControlMode": "制御モード", + "infillColorValue": "描画色", + "coherenceMinDenoise": "最小ノイズ除去", + "setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (おそらく小さすぎます)", + "cfgRescaleMultiplier": "CFGリスケール倍率", + "clipSkip": "クリップスキップ", + "guidance": "ガイダンス", + "infillMethod": "インフィル方式", + "patchmatchDownScaleSize": "ダウンスケール", + "boxBlur": "ボックスぼかし", + "remixImage": "画像をリミックス", + "processImage": "画像処理を実行", + "useCpuNoise": "CPUノイズの使用", + "staged": "ステージ", + "perlinNoise": "パーリン・ノイズ(グラデーションノイズ)", + "imageActions": "その他のアクション", + "gaussianBlur": "ガウスぼかし", + "noiseThreshold": "ノイズの閾値", + "maskBlur": "マスクぼかし", + "seamlessYAxis": "垂直方向シームレス", + "optimizedImageToImage": "イメージ to イメージの最適化", + "symmetry": "左右対称", + "seamlessXAxis": "水平方向シームレス", + "sendToCanvas": "キャンバスに送る", + "modelDisabledForTrial": "{{modelName}} を使用した生成はトライアルアカウントではご利用いただけません.アップグレードするには,アカウント設定 にアクセスしてください.", + "duration": "間隔", + "downloadImage": "画像をダウンロード", + "images_withCount_other": "画像", + "showOptionsPanel": "サイドパネルを表示(O または T)", + "useClipSkip": "CLIPスキップを使用する", + "resolution": "解像度", + "colorCompensation": "色補正", + "imageSize": "画像サイズ", + "disabledNotSupported": "モデルがサポートしていません" + }, + "settings": { + "models": "モデル", + "displayInProgress": "生成中の画像を表示する", + "confirmOnDelete": "削除時に確認する", + "resetWebUI": "WebUIをリセット", + "resetWebUIDesc1": "WebUIのリセットは、ブラウザローカルキャッシュの画像と設定のリセットします。ディスク内の画像は削除されません。", + "resetWebUIDesc2": "もしギャラリーに画像が表示されないなど、何か問題が発生した場合はGitHubにissueを提出する前にリセットを試してください。", + "resetComplete": "WebUIはリセットされました。", + "ui": "ユーザーインターフェイス", + "beta": "ベータ", + "developer": "デベロッパー", + "antialiasProgressImages": "生成中の画像にアンチエイリアスを適用する", + "enableInformationalPopovers": "情報ポップオーバーを有効にする", + "enableModelDescriptions": "ドロップダウンでモデルの説明を有効にする", + "confirmOnNewSession": "新しいセッションで確認する", + "informationalPopoversDisabled": "情報ポップオーバーが無効になっています", + "informationalPopoversDisabledDesc": "情報ポップオーバーが無効になっています。設定で有効にしてください。", + "enableNSFWChecker": "NSFWチェッカーを有効にする", + "enableInvisibleWatermark": "不可視のウォーターマークを有効にする", + "enableHighlightFocusedRegions": "フォーカスエリアを強調表示", + "clearIntermediatesDesc1": "中間生成物をクリアすると、キャンバスやコントロールレイヤーの状態がリセットされます。", + "showProgressInViewer": "ビューアで生成状況を表示する", + "clearIntermediatesDisabled": "中間生成物をクリアするには、キューが空でなければなりません", + "clearIntermediatesDesc2": "中間生成物とは、生成工程内で利用された内部画像です。ギャラリー内の画像とは異なります。中間生成物を削除するとディスク容量が解放されます。", + "intermediatesClearedFailed": "中間生成物のクリア中に問題が発生しました", + "reloadingIn": "リロード中", + "clearIntermediatesDesc3": "ギャラリーの画像は削除されません。", + "clearIntermediates": "中間生成物をクリア", + "clearIntermediatesWithCount_other": "{{count}} 個の中間生成物をクリア", + "intermediatesCleared_other": "{{count}}個の中間生成物がクリアされました", + "general": "一般", + "generation": "生成", + "showDetailedInvocationProgress": "生成状況の詳細を表示", + "modelDescriptionsDisabled": "ドロップダウンのモデル説明が無効になっています", + "modelDescriptionsDisabledDesc": "ドロップダウンのモデル説明が無効になっています。設定で有効にしてください。", + "externalProviderConfigured": "設定済み", + "externalProviderNotConfigured": "APIキーが必要", + "externalProviderNotConfiguredHint": "このプロバイダを有効にするには、モデルマネージャまたはサーバー設定で API キーを追加してください。", + "externalProviders": "外部プロバイダー", + "maxQueueHistory": "キュー履歴の最大数", + "maxQueueHistorySaveFailed": "キュー履歴の最大数の保存に失敗", + "imageSubfolderStrategy": "画像保存サブフォルダーの構成", + "imageSubfolderStrategyDate": "日付", + "imageSubfolderStrategyFlat": "フラット", + "imageSubfolderStrategyHash": "ハッシュ", + "imageSubfolderStrategySaveFailed": "画像保存サブフォルダーの構成設定に失敗しました", + "imageSubfolderStrategyType": "タイプ", + "imageSubfolderStrategyUnknown": "不明な ({{strategy}})", + "prompt": "プロンプト", + "preferAttentionStyleNumeric": "数字による重みづけスタイル", + "middleClickOpenInNewTab": "ミドルクリックで画像を新しいタブで開く" + }, + "toast": { + "uploadFailed": "アップロード失敗", + "imageCopied": "画像がコピーされました", + "imageUploadFailed": "画像のアップロードに失敗しました", + "uploadFailedInvalidUploadDesc": "画像はPNGかJPGかWEBPである必要があります .", + "sentToUpscale": "アップスケーラーに転送しました", + "imageUploaded": "画像をアップロードしました", + "serverError": "サーバーエラー", + "prunedQueue": "キューを破棄", + "workflowDeleted": "ワークフローが削除されました", + "unableToLoadStylePreset": "スタイルプリセットをロードできません", + "loadedWithWarnings": "ワークフローが警告付きでロードされました", + "parameters": "パラメーター", + "parameterSet": "パラメーターが呼び出されました", + "pasteSuccess": "{{destination}} に貼り付けました", + "imagesWillBeAddedTo": "アップロードされた画像はボード {{boardName}} のアセットに追加されます.", + "layerCopiedToClipboard": "レイヤーがクリップボードにコピーされました", + "pasteFailed": "貼り付け失敗", + "importSuccessful": "インポートが成功しました", + "problemDownloadingImage": "画像をダウンロードできません", + "modelAddedSimple": "モデルがキューに追加されました", + "outOfMemoryErrorDesc": "現在の生成設定はシステム容量を超えています.設定を調整してもう一度お試しください.", + "parametersSet": "パラメーターが呼び出されました", + "modelImportCanceled": "モデルのインポートがキャンセルされました", + "problemRetrievingWorkflow": "ワークフローを取得した問題", + "problemUnpublishingWorkflow": "取り消されたワークフローの問題", + "parametersNotSet": "パラメーターが呼び出されていません", + "problemCopyingImage": "画像をコピーできません", + "baseModelChanged": "ベースモデルが変更されました", + "baseModelChangedCleared_other": "互換性のないサブモデル {{count}} 個を更新、クリア、または無効化しました", + "canceled": "処理がキャンセルされました", + "connected": "サーバーに接続されました", + "linkCopied": "リンクがコピーされました", + "unableToLoadImage": "画像をロードできません", + "unableToLoadImageMetadata": "画像のメタデータをロードできません", + "importFailed": "インポートに失敗しました", + "outOfMemoryError": "メモリ不足エラー", + "parameterSetDesc": "{{parameter}}を呼び出し", + "errorCopied": "エラーがコピーされました", + "sentToCanvas": "キャンバスに送信", + "workflowLoaded": "ワークフローがロードされました", + "unableToCopy": "コピーできません", + "unableToCopyDesc": "あなたのブラウザはクリップボードアクセスをサポートしていません.Firefoxユーザーの場合は、以下の手順で修正できる可能性があります. ", + "fluxFillIncompatibleWithT2IAndI2I": "FLUX Fillは、テキストから画像・画像から画像への変換機能と互換性がありません。これらのタスクには他のFLUXモデルをご利用ください。", + "problemUnpublishingWorkflowDescription": "取り下げられたワークフローの問題がありました.もう一度試してください.", + "workflowUnpublished": "ワークフローが取り消されました", + "sessionRef": "セッション: {{sessionId}}", + "somethingWentWrong": "問題が発生しました", + "unableToCopyDesc_theseSteps": "ステップ", + "stylePresetLoaded": "スタイルプリセットがロードされました", + "parameterNotSetDescWithMessage": "{{parameter}}: {{message}}を呼び出せません", + "problemCopyingLayer": "レイヤーをコピーできません", + "problemSavingLayer": "レイヤー保存ができません", + "outOfMemoryErrorDescLocal": "OOM を削減するには、低 VRAM ガイド に従ってください.", + "parameterNotSet": "パラメーターが呼び出されていません", + "addedToBoard": "ボード {{name}} にアセットを追加しました", + "addedToUncategorized": "ボード $t(boards.uncategorized) にアセットが追加されました", + "problemDeletingWorkflow": "ワークフローが削除された問題", + "parameterNotSetDesc": "{{parameter}}を呼び出せません", + "chatGPT4oIncompatibleGenerationMode": "ChatGPT 4oは,テキストから画像への生成と画像から画像への生成のみをサポートしています.インペインティングおよび,アウトペインティングタスクには他のモデルを使用してください.", + "imagenIncompatibleGenerationMode": "Google {{model}} はテキストから画像への変換のみをサポートしています. 画像から画像への変換, インペインティング,アウトペインティングのタスクには他のモデルを使用してください.", + "noVisibleRasterLayers": "表示されるラスター レイヤーがありません", + "noVisibleRasterLayersDesc": "PSD にエクスポートするには、少なくとも 1 つのラスター レイヤーを有効にします", + "invalidCanvasDimensions": "キャンバスのサイズが無効です", + "canvasTooLarge": "キャンバスが大きすぎます", + "canvasTooLargeDesc": "キャンバスのサイズがPSDエクスポートの最大許容サイズを超えています。キャンバス全体の幅と高さを小さくしてから、もう一度お試しください。", + "psdExportSuccess": "PSDエクスポート完了", + "psdExportSuccessDesc": "{{count}} 個のレイヤーを PSD ファイルに正常にエクスポートしました", + "problemExportingPSD": "PSD のエクスポート中に問題が発生しました", + "canvasManagerNotAvailable": "キャンバスマネージャーは利用できません", + "fluxKontextIncompatibleGenerationMode": "FLUX Kontextはキャンバス上に配置された画像からの生成をサポートしていません。参照画像を使用して再試行し、ラスターレイヤーも無効にしてください。", + "promptGenerationStarted": "プロンプト生成が開始されました", + "uploadAndPromptGenerationFailed": "画像のアップロードとプロンプトの生成に失敗しました", + "promptExpansionFailed": "問題が発生しました。プロンプトの展開をもう一度お試しください。", + "imageNotLoadedDesc": "画像を見つけられません", + "imageSaved": "画像を保存しました", + "imageSavingFailed": "画像の保存に失敗しました", + "invalidUpload": "無効なアップロードです", + "layerSavedToAssets": "レイヤーがアセットに保存されました", + "noRasterLayers": "ラスターレイヤーが見つかりません", + "noRasterLayersDesc": "PSDにエクスポートするには、少なくとも1つのラスターレイヤーを作成します", + "noActiveRasterLayers": "アクティブなラスターレーヤーがありません", + "noActiveRasterLayersDesc": "PSDにエクスポートするには、少なくとも1つのラスターレイヤーを有効にします", + "failedToProcessLayers": "レイヤーの処理に失敗しました", + "noValidLayerAdapters": "有効なレイヤーアダプタが見つかりません", + "setControlImage": "制御画像として設定", + "setNodeField": "ノードフィールドとして設定", + "uploadFailedInvalidUploadDesc_withCount_other": "最大 {{count}} 個の PNG、JPEG、または WEBP 画像を使用する必要があります。", + "maskInverted": "マスク反転", + "maskInvertFailed": "マスクの反転に失敗しました", + "noVisibleMasks": "表示されているマスクがありません", + "noVisibleMasksDesc": "反転するには、少なくとも1つのインペイントマスクを作成または有効にしてください", + "noInpaintMaskSelected": "インペイントマスクが選択されていません", + "noInpaintMaskSelectedDesc": "反転するインペイントマスクを選択", + "invalidBbox": "無効なバウンディングボックス", + "invalidBboxDesc": "バウンディングボックスの寸法が有効ではありません", + "modelDownloadPaused": "モデルダウンロードの一時停止中", + "modelDownloadResumed": "ダウンロード再開", + "modelDownloadRestartFailed": "失敗したダウンロードを再開", + "modelDownloadRestartFile": "ファイルダウンロードの再開中", + "modelDownloadRestartedFromScratch": "一部のファイルが見つかりません。 最初からダウンロードを再開しました。", + "schedulerReset": "スケジューラをリセット" + }, + "accessibility": { + "invokeProgressBar": "進捗バー", + "reset": "リセット", + "uploadImage": "画像をアップロード", + "previousImage": "前の画像", + "nextImage": "次の画像", + "menu": "メニュー", + "createIssue": "問題を報告", + "resetUI": "$t(accessibility.reset) UI", + "mode": "モード", + "about": "Invoke について", + "submitSupportTicket": "サポート依頼を送信する", + "uploadImages": "画像をアップロード", + "toggleLeftPanel": "左パネルをトグル (T)", + "toggleRightPanel": "右パネルをトグル (G)" + }, + "metadata": { + "Threshold": "ノイズ閾値", + "seed": "シード", + "width": "幅", + "workflow": "ワークフロー", + "steps": "ステップ", + "scheduler": "スケジューラ", + "positivePrompt": "ポジティブプロンプト", + "strength": "Image to Image 強度", + "recallParameters": "パラメータを再使用", + "imageDimensions": "画像サイズ", + "imageDetails": "画像の詳細", + "model": "モデル", + "allPrompts": "すべてのプロンプト", + "cfgScale": "CFGスケール", + "createdBy": "作成:", + "metadata": "メタデータ", + "height": "高さ", + "negativePrompt": "ネガティブプロンプト", + "generationMode": "生成モード", + "vae": "VAE", + "cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)", + "canvasV2Metadata": "キャンバス・レイヤー", + "guidance": "手引き", + "seamlessXAxis": "シームレスX軸", + "seamlessYAxis": "シームレスY軸", + "parameterSet": "パラメーター {{parameter}} が設定されました", + "noMetaData": "メタデータが見つかりません", + "noRecallParameters": "呼び出されたパラメーターが見つかりません", + "noImageDetails": "画像の詳細が見つかりません", + "clipSkip": "$t(parameters.clipSkip)", + "parsingFailed": "解析に失敗しました", + "recallParameter": "{{label}} をリコール", + "qwen3Encoder": "Qwen3 エンコーダー", + "qwen3Source": "Qwen3 ソース", + "seedVarianceEnabled": "シードバリアンスが有効", + "seedVarianceStrength": "シードバリアンス強度", + "seedVarianceRandomizePercent": "シードバリアンスのランダム化パーセンテージ", + "zImageShift": "Z-Image シフト", + "geminiTemperature": "Gemini Temperature", + "geminiThinkingLevel": "Gemini Thinking Level", + "openaiQuality": "OpenAI 品質", + "imageSize": "画像サイズ" + }, + "queue": { + "queueEmpty": "キューが空です", + "pauseSucceeded": "処理が一時停止されました", + "queueFront": "キューの先頭へ追加", + "queueBack": "キューに追加", + "pause": "一時停止", + "queue": "キュー", + "pauseTooltip": "処理を一時停止", + "cancel": "キャンセル", + "resumeSucceeded": "処理が再開されました", + "resumeTooltip": "処理を再開", + "resume": "再開", + "status": "ステータス", + "pruneSucceeded": "キューから完了アイテム{{item_count}}件を削除しました", + "cancelTooltip": "現在のアイテムをキャンセル", + "in_progress": "進行中", + "notReady": "キューに追加できません", + "batchFailedToQueue": "バッチをキューに追加できませんでした", + "completed": "完了", + "cancelFailed": "アイテムのキャンセルに問題があります", + "batchQueued": "バッチをキューに追加しました", + "pauseFailed": "処理の一時停止に問題があります", + "clearFailed": "キューのクリアに問題があります", + "front": "先頭", + "clearSucceeded": "キューがクリアされました", + "pruneTooltip": "{{item_count}} の完了アイテムを削除", + "cancelSucceeded": "アイテムがキャンセルされました", + "batchQueuedDesc_other": "{{count}} セッションをキューの{{direction}}に追加しました", + "graphQueued": "グラフをキューに追加しました", + "batch": "バッチ", + "clearQueueAlertDialog": "キューをクリアすると、処理中の項目は直ちにキャンセルされ、キューは完全にクリアされます。保留中のフィルターもキャンセルされ、ステージングエリアもリセットされます。", + "pending": "保留中", + "resumeFailed": "処理の再開に問題があります", + "clear": "クリア", + "total": "合計", + "canceled": "キャンセル", + "pruneFailed": "キューの削除に問題があります", + "cancelBatchSucceeded": "バッチがキャンセルされました", + "clearTooltip": "全てのアイテムをキャンセルしてクリア", + "current": "現在", + "failed": "失敗", + "cancelItem": "項目をキャンセル", + "next": "次", + "cancelBatch": "バッチをキャンセル", + "session": "セッション", + "enqueueing": "バッチをキューに追加", + "cancelBatchFailed": "バッチのキャンセルに問題があります", + "clearQueueAlertDialog2": "キューをクリアしてもよろしいですか?", + "item": "項目", + "graphFailedToQueue": "グラフをキューに追加できませんでした", + "openQueue": "キューを開く", + "time": "時間", + "completedIn": "完了まで", + "back": "戻る", + "prune": "完了を削除", + "prompts_other": "プロンプト", + "iterations_other": "イテレーション", + "generations_other": "生成", + "canvas": "キャンバス", + "workflows": "ワークフロー", + "upscaling": "アップスケール", + "generation": "生成", + "other": "その他", + "gallery": "ギャラリー", + "cancelAllExceptCurrentQueueItemAlertDialog2": "すべての保留中のキュー項目をキャンセルしてもよいですか?", + "cancelAllExceptCurrentTooltip": "現在のアイテム以外すべてキャンセル", + "origin": "先頭", + "destination": "出力先", + "confirm": "確認", + "retryItem": "項目をリトライ", + "batchSize": "バッチサイズ", + "retryFailed": "項目のリトライに問題があります", + "cancelAllExceptCurrentQueueItemAlertDialog": "現在のアイテム以外のすべてのキューをキャンセルすると、保留中アイテムは停止しますが、進行中アイテムは完了します。", + "retrySucceeded": "項目がリトライされました", + "credits": "クレジット", + "cancelAllExceptCurrent": "現在選択中のもの以外はすべてキャンセル", + "batchFieldValues": "バッチフィールド値", + "createdAt": "作成日", + "completedAt": "完了日時", + "sortColumn": "列の並べ替え", + "sortBy": "{{column}}で並べ替え", + "sortOrderAscending": "昇順", + "sortOrderDescending": "降順", + "cancelFailedAccessDenied": "アイテムのキャンセル中に問題が発生しました:アクセスが拒否されました", + "clearFailedAccessDenied": "キューのクリア中に問題が発生しました:アクセスが拒否されました", + "paused": "一時停止中", + "user": "ユーザー", + "fieldValuesHidden": "<非表示>", + "cannotViewDetails": "このキューアイテムを閲覧する権限がありません", + "queueActionsMenu": "アクションメニューをキュー", + "queueItem": "アイテムをキュー" + }, + "models": { + "noMatchingModels": "一致するモデルがありません", + "loading": "読み込み中", + "noModelsAvailable": "使用可能なモデルがありません", + "selectModel": "モデルを選択してください", + "concepts": "コンセプト", + "addLora": "LoRAを追加", + "lora": "LoRA", + "defaultVAE": "デフォルトVAE", + "noRefinerModelsInstalled": "インストールされているSDXLリファイナーモデルはありません", + "noCompatibleLoRAs": "互換性のあるLoRAはありません", + "noMatchingLoRAs": "一致するLoRAがありません", + "noLoRAsInstalled": "LoRAがインストールされていません" + }, + "nodes": { + "addNode": "ノードを追加", + "boolean": "ブーリアン", + "addNodeToolTip": "ノードを追加 (Shift+A, Space)", + "missingTemplate": "Invalid node: タイプ {{type}} のノード {{node}} にテンプレートがありません(未インストール?)", + "loadWorkflow": "ワークフローを読み込み", + "float": "浮動小数点", + "integer": "整数", + "nodeTemplate": "ノードテンプレート", + "inputMayOnlyHaveOneConnection": "入力は1つの接続しか持つことができません", + "nodeOutputs": "ノード出力", + "currentImageDescription": "ノードエディタ内の現在の画像を表示", + "downloadWorkflow": "ワークフローのJSONをダウンロード", + "fieldTypesMustMatch": "フィールドタイプが一致している必要があります", + "edge": "エッジ", + "animatedEdgesHelp": "選択したエッジ、選択したノードに接続されたエッジをアニメーション表示", + "cannotDuplicateConnection": "重複した接続は作れません", + "noWorkflow": "ワークフローがありません", + "fullyContainNodesHelp": "選択ボックス内に完全に含まれているノードだけが選択されます", + "nodeType": "ノードタイプ", + "executionStateInProgress": "処理中", + "executionStateError": "エラー", + "hideMinimapnodes": "ミニマップを非表示", + "fitViewportNodes": "全体を表示", + "executionStateCompleted": "完了", + "node": "ノード", + "currentImage": "現在の画像", + "collection": "コレクション", + "cannotConnectInputToInput": "入力から入力には接続できません", + "cannotConnectOutputToOutput": "出力から出力には接続できません", + "cannotConnectToSelf": "自身のノードには接続できません", + "colorCodeEdges": "エッジのカラー", + "loadingNodes": "ノードを読み込み中...", + "scheduler": "スケジューラ", + "version": "バージョン", + "edit": "編集", + "nodeVersion": "ノードバージョン", + "workflowTags": "タグ", + "string": "文字列", + "workflowVersion": "バージョン", + "workflowAuthor": "作者", + "ipAdapter": "IP-Adapter", + "notes": "ノート", + "workflow": "ワークフロー", + "workflowName": "名前", + "workflowNotes": "ノート", + "enum": "Enum", + "arithmeticSequence": "等差数列", + "linearDistribution": "線形分布", + "animatedEdges": "エッジのアニメーション", + "uniformRandomDistribution": "一様ランダム分布", + "noBatchGroup": "グループなし", + "parseString": "文字列の解析", + "generatorImagesFromBoard": "ボードからの画像", + "missingNode": "呼び出しノードがありません", + "missingSourceOrTargetNode": "ソースまたはターゲットノードがありません", + "missingSourceOrTargetHandle": "ソースまたはターゲットハンドルがありません", + "fullyContainNodes": "完全に囲んだノードを選択する", + "noWorkflows": "ワークフローがありません", + "nodeSearch": "ノードを検索", + "showEdgeLabels": "エッジのラベルを表示", + "downloadWorkflowError": "ワークフローのダウンロード中にエラーが発生しました", + "generatorNRandomValues_other": "{{count}} 個のランダムな値", + "dynamicPromptsRandom": "ダイナミックプロンプト(ランダム)", + "generatorLoadFromFile": "ファイルから読み込み", + "connectionWouldCreateCycle": "コネクションはサイクルをつくります", + "singleFieldType": "{{name}} (単数)", + "targetNodeDoesNotExist": "無効なエッジ:ターゲット/インプットノード {{node}} 存在しません", + "noConnectionInProgress": "進捗中のコネクションはありません", + "generatorImagesCategory": "カテゴリー", + "generatorImages_other": "{{count}} 個の画像", + "missingInvocationTemplate": "呼び出しテンプレートがありません", + "nodePack": "ノードパック", + "targetNodeFieldDoesNotExist": "無効なエッジ:ターゲット/インプットフィールド{{node}}.{{field}} が存在しません", + "dynamicPromptsCombinatorial": "ダイナミックプロンプト(組み合わせ)", + "cannotMixAndMatchCollectionItemTypes": "コレクション・アイテムの種類を組み合わせることはできません", + "missingFieldTemplate": "フィールドテンプレートがありません", + "editMode": "ワークフローエディタを編集します", + "sourceNodeDoesNotExist": "無効なエッジ:ソース/アウトプットノード{{node}}が存在しません", + "generatorNoValues": "空", + "collectionOrScalarFieldType": "{{name}} (単数またはコレクション)", + "unableToUpdateNode": "ノードアップロード失敗:ノード {{node}} のタイプ {{type}} (削除か再生成が必要かもしれません)", + "deletedInvalidEdge": "無効なエッジを削除しました{{source}} -> {{target}}", + "collectionFieldType": "{{name}} (コレクション)", + "colorCodeEdgesHelp": "接続されたフィールドのタイプごとにエッジの色を変える", + "showEdgeLabelsHelp": "エッジに接続されているノードをラベル表示", + "sourceNodeFieldDoesNotExist": "無効なエッジ:ソース/アウトプットフィールド{{node}}.{{field}}が存在しません", + "deletedMissingNodeFieldFormElement": "不足しているフォームフィールドを削除しました: ノード {{nodeId}} フィールド {{fieldName}}", + "nodeName": "ノード名", + "splitOn": "分割オン", + "noMatchingWorkflows": "一致するワークフローがありません", + "unknownNodeType": "不明なノード型", + "inputFieldTypeParseError": "入力フィールド{{node}}.{{field}}の型を解析できません ({{message}})", + "loadWorkflowDesc": "ワークフローを読み込みますか?", + "loadWorkflowDesc2": "現在のワークフローには保存されていない変更があります。", + "clearWorkflowDesc": "このワークフローをクリアして新しいワークフローにしますか?", + "updateNode": "ノードをアップデート", + "graph": "グラフ", + "workflowContact": "問い合わせ先", + "outputFieldTypeParseError": "出力フィールド {{node}}.{{field}} の型を解析できません({{message}})", + "unableToExtractEnumOptions": "enum オプションを抽出できません", + "zoomOutNodes": "縮小", + "unableToGetWorkflowVersion": "ワークフローのスキーマバージョンを取得できません", + "missingField_withName": "欠落しているフィールド \"{{name}}\"", + "zoomInNodes": "拡大", + "addItem": "項目を追加", + "boardAccessError": "ボード {{board_id}}が見つからないので,デフォルトにリセットします", + "unknownNode": "不明なノード", + "imageAccessError": "画像{{image_name}}が見つからないので,デフォルトにリセットします", + "prototypeDesc": "この呼び出しはプロトタイプです.アプリの更新時に変更される可能性があり,いつでも削除される可能性があります.", + "reloadNodeTemplates": "ノードテンプレートを再読み込み", + "snapToGridHelp": "移動時にノードをグリッドにスナップ", + "unableToExtractSchemaNameFromRef": "参照からスキーマ名を抽出できません", + "unableToUpdateNodes_other": "{{count}} 個のノードをアップデートできません", + "workflowSettings": "ワークフローエディター設定", + "specialDesc": "この呼び出しは,アプリ内で特別な処理を行います.例えば,バッチノードは1つのワークフローから複数のグラフをキューに入れるために使用されます.", + "modelAccessError": "モデル {{key}}が見つからないので,デフォルトにリセットします", + "betaDesc": "この呼び出しはベータ版です.安定するまでは,アプリのアップデートの際に変更される可能性があります.この呼び出しは長期的にサポートする予定です.", + "internalDesc": "この呼び出しはInvokeによって内部的に使用されます。アプリの更新時に変更される可能性があり、いつでも削除される可能性があります。", + "noFieldsViewMode": "このワークフローには表示する選択フィールドがありません.値を設定するためにはワークフロー全体を表示します.", + "clearWorkflow": "ワークフローをクリア", + "snapToGrid": "グリッドにスナップ", + "showMinimapnodes": "ミニマップを表示", + "description": "説明", + "notesDescription": "ワークフローに関するメモを追加する", + "newWorkflowDesc2": "現在のワークフローには保存されていない変更があります。", + "unknownField": "不明なフィールド", + "unexpectedField_withName": "予期しないフィールド\"{{name}}\"", + "validateConnectionsHelp": "無効な接続が行われたり,無効なグラフが呼び出されたりしないようにします", + "validateConnections": "接続とグラフを確認する", + "saveToGallery": "ギャラリーに保存", + "newWorkflowDesc": "新しいワークフローを作りますか?", + "unknownFieldType": "$t(nodes.unknownField)型: {{type}}", + "unsupportedArrayItemType": "サポートされていない配列項目型です \"{{type}}\"", + "unableToValidateWorkflow": "ワークフローを確認できません", + "unknownErrorValidatingWorkflow": "ワークフローの確認で不明なエラーが発生", + "clearWorkflowDesc2": "現在のワークフローには保存されていない変更があります。", + "unsupportedMismatchedUnion": "CollectionOrScalar型とベース型{{firstType}}および{{secondType}}が不一致です", + "updateApp": "アプリケーションをアップデート", + "noGraph": "グラフなし", + "unsupportedAnyOfLength": "結合したメンバーが多すぎます ({{count}})", + "updateAllNodes": "ノードをアップデート", + "allNodesUpdated": "全てのノードをアップデート", + "workflowHelpText": "ヘルプはGetting Started with Workflows のガイドをご覧ください.", + "noNodeSelected": "選択されたノードがありません", + "problemSettingTitle": "問題設定のタイトル", + "resetToDefaultValue": "デフォルト値にリセット", + "newWorkflow": "新しいワークフロー", + "unknownField_withName": "不明なフィールド\"{{name}}\"", + "unknownFieldEditWorkflowToFix_withName": "ワークフローは不明なフィールドを含んでいます \"{{name}}\".\n問題を修正するためにワークフローを編集してください.", + "viewMode": "線形ビューでの使用", + "workflowDescription": "概要", + "workflowValidation": "ワークフロー検証エラー", + "noOutputRecorded": "記録されたアウトプットがありません", + "nodeOpacity": "ノードの不透明度", + "unableToParseFieldType": "フィールドタイプを解析できません", + "generatorLoading": "ローディング", + "addLinearView": "リニアビューに追加", + "hideLegendNodes": "フィールドタイプの凡例を非表示", + "mismatchedVersion": "無効なノード: タイプ {{type}} のノード {{node}} のバージョンが一致しません (更新してみてください)", + "noFieldsLinearview": "リニアビューにフィールドが追加されていません", + "removeLinearView": "リニアビューから削除", + "reorderLinearView": "リニアビューの並べ替え", + "showLegendNodes": "フィールドタイプの凡例を表示", + "unableToLoadWorkflow": "ワークフローを読み込めません", + "unknownTemplate": "不明なテンプレート", + "unknownInput": "不明な入力: {{name}}", + "loadingTemplates": "{{name}}を読み込んでいます", + "versionUnknown": " バージョン不明", + "generateValues": "値を生成する", + "floatRangeGenerator": "浮動小数点範囲ジェネレータ", + "integerRangeGenerator": "整数範囲ジェネレータ", + "layout": { + "autoLayout": "自動レイアウト", + "layeringStrategy": "レイヤリング戦略", + "networkSimplex": "ネットワーク・シンプレックス", + "longestPath": "最長経路", + "nodeSpacing": "ノード間隔", + "layerSpacing": "レイヤー間隔", + "layoutDirection": "レイアウト方向", + "layoutDirectionRight": "右", + "layoutDirectionDown": "下", + "alignment": "ノードの配置", + "alignmentUL": "左上", + "alignmentDL": "左下", + "alignmentUR": "右上", + "alignmentDR": "右下" + }, + "noWorkflowToSave": "保存するワークフローがありません", + "groupNodesByCategory": "ノードをカテゴリごとに表示", + "groupNodesByCategoryHelp": "ノード追加ダイアログ内で、ノードをカテゴリ別に表示" + }, + "boards": { + "autoAddBoard": "自動追加するボード", + "move": "移動", + "menuItemAutoAdd": "このボードに自動追加", + "myBoard": "マイボード", + "searchBoard": "ボードを検索...", + "noMatching": "一致するボードがありません", + "selectBoard": "ボードを選択", + "cancel": "キャンセル", + "addBoard": "ボードを追加", + "uncategorized": "未分類", + "downloadBoard": "ボードをダウンロード", + "changeBoard": "ボードを変更", + "loading": "ロード中...", + "topMessage": "この選択には、次の機能で使用される画像が含まれています:", + "bottomMessage": "この画像を削除すると、現在利用している機能はリセットされます。", + "clearSearch": "検索をクリア", + "deleteBoard": "ボードの削除", + "deleteBoardAndImages": "ボードと画像の削除", + "deleteBoardOnly": "ボードのみ削除", + "deletedBoardsCannotbeRestored": "削除したボードと画像は復元できません。「ボードのみ削除」を選択すると、画像は未分類の状態になります。", + "movingImagesToBoard_other": "{{count}} の画像をボードに移動:", + "assetsWithCount_other": "{{count}} のアセット", + "addPrivateBoard": "プライベートボードを追加", + "addSharedBoard": "共有ボードを追加", + "boards": "ボード", + "private": "プライベートボード", + "shared": "共有ボード", + "archiveBoard": "ボードをアーカイブ", + "archived": "アーカイブ完了", + "unarchiveBoard": "アーカイブされていないボード", + "imagesWithCount_other": "{{count}} の画像", + "updateBoardError": "ボード更新エラー", + "selectedForAutoAdd": "自動追加に選択済み", + "deletedPrivateBoardsCannotbeRestored": "削除されたボードと画像は復元できません。「ボードのみ削除」を選択すると、画像は作成者に対して非公開の未分類状態になります。", + "noBoards": "{{boardType}} ボードがありません", + "uncategorizedImages": "分類されていない画像", + "deleteAllUncategorizedImages": "分類されていないすべての画像を削除", + "deletedImagesCannotBeRestored": "削除された画像は復元できません。", + "hideBoards": "ボードを隠す", + "locateInGalery": "ギャラリーで検索", + "viewBoards": "ボードを表示", + "pause": "一時停止", + "resume": "再開", + "restartFailed": "再起動に失敗しました", + "restartFile": "ファイルを再起動", + "restartRequired": "再起動が必要です", + "resumeRefused": "サーバーで再開が拒否されました。再起動が必要です。", + "setBoardVisibility": "ボードの表示を設定", + "setVisibilityPrivate": "プライベートに設定", + "setVisibilityShared": "シェアに設定", + "setVisibilityPublic": "パブリックに設定", + "visibilityPrivate": "プライベート", + "visibilityShared": "シェア済み", + "visibilityPublic": "パブリック", + "visibilityBadgeShared": "シェア済みのボード", + "visibilityBadgePublic": "パブリックのボード", + "updateBoardVisibilityError": "ボード表示設定の変更中にエラーがありました" + }, + "invocationCache": { + "invocationCache": "呼び出しキャッシュ", + "clearSucceeded": "呼び出しキャッシュをクリアしました", + "clearFailed": "呼び出しキャッシュのクリアに問題があります", + "enable": "有効", + "clear": "クリア", + "maxCacheSize": "最大キャッシュサイズ", + "cacheSize": "キャッシュサイズ", + "useCache": "キャッシュを使用", + "misses": "見つからないキャッシュ", + "hits": "見つかったキャッシュ", + "disableSucceeded": "呼び出しキャッシュが無効", + "disableFailed": "呼び出しキャッシュの無効化中に問題が発生", + "enableSucceeded": "呼び出しキャッシュが有効", + "disable": "無効", + "enableFailed": "呼び出しキャッシュの有効化中に問題が発生" + }, + "popovers": { + "paramRatio": { + "heading": "縦横比", + "paragraphs": [ + "生成された画像の縦横比。", + "SD1.5 モデルの場合は 512x512 に相当する画像サイズ (ピクセル数) が推奨され, SDXL モデルの場合は 1024x1024 に相当するサイズが推奨されます." + ] + }, + "regionalGuidanceAndReferenceImage": { + "heading": "領域ガイダンスと領域参照画像", + "paragraphs": [ + "領域ガイダンスの場合は,ブラシを使用して,グローバルプロンプトの要素が表示される場所をガイドします.", + "領域参照画像の場合は,ブラシを使用して特定の領域に参照画像を適用します." + ] + }, + "regionalReferenceImage": { + "heading": "領域参照画像", + "paragraphs": [ + "ブラシで指定した範囲に参照画像を適用します。" + ] + }, + "paramScheduler": { + "heading": "スケジューラ", + "paragraphs": [ + "スケジューラは生成処理中に利用されます。", + "各スケジューラは、画像にノイズを反復的に追加する方法や、モデルの出力に基づいてサンプルを更新する方法を定義します." + ] + }, + "regionalGuidance": { + "heading": "領域ガイダンス", + "paragraphs": [ + "グローバルプロンプトの要素が表示される場所をガイドするブラシ." + ] + }, + "rasterLayer": { + "heading": "ラスターレイヤー", + "paragraphs": [ + "画像生成中に使用される,キャンバスのピクセルベースのコンテンツ." + ] + }, + "globalReferenceImage": { + "heading": "全域参照画像", + "paragraphs": [ + "参照画像で画像全体に影響を及ぼします。" + ] + }, + "paramUpscaleMethod": { + "heading": "アップスケール手法", + "paragraphs": [ + "高解像度修正のために画像を拡大するために使用される方法。" + ] + }, + "upscaleModel": { + "heading": "アップスケールモデル", + "paragraphs": [ + "アップスケールモデルは、ディテールを追加する前に画像を出力サイズに合わせて拡大縮小します。サポートされているアップスケールモデルであればどれでも使用できますが、写真や線画など、特定の種類の画像に特化したモデルもあります。" + ] + }, + "paramAspect": { + "heading": "縦横比", + "paragraphs": [ + "生成される画像のアスペクト比。比率を変更すると、幅と高さもそれに応じて更新されます。", + "「最適化」は、選択したモデルの幅と高さを最適な寸法に設定します。" + ] + }, + "refinerSteps": { + "heading": "ステップ", + "paragraphs": [ + "生成プロセスのリファイナー部分で実行されるステップの数。", + "生成ステップと似ています。" + ] + }, + "paramVAE": { + "heading": "VAE", + "paragraphs": [ + "AI 出力を最終画像に変換するために使用されるモデル。" + ] + }, + "scale": { + "heading": "スケール", + "paragraphs": [ + "スケールは出力画像のサイズを制御し、入力画像の解像度の倍数に基づいて決定されます。例えば、1024x1024の画像を2倍に拡大すると、2048x2048の出力が生成されます。" + ] + }, + "refinerScheduler": { + "heading": "スケジューラ", + "paragraphs": [ + "生成プロセスのリファイナー部分で使用されるスケジューラ。", + "生成スケジューラに似ています。" + ] + }, + "compositingCoherenceMode": { + "heading": "モード", + "paragraphs": [ + "新しく生成されたマスク領域と,一貫性のある画像を作成するために使用される方法." + ] + }, + "paramModel": { + "heading": "モデル", + "paragraphs": [ + "生成に使用するメインモデル。各モデルは、それぞれに特化したテイストやコンテンツが出力される様にチューニングされています。" + ] + }, + "paramHeight": { + "heading": "高さ", + "paragraphs": [ + "生成される画像の高さ。8の倍数にする必要があります。" + ] + }, + "paramSteps": { + "heading": "ステップ", + "paragraphs": [ + "各生成で実行されるステップの数.", + "基本的にステップが多いほどより高品質な画像が作成されますが、生成時間も長くなります。" + ] + }, + "ipAdapterMethod": { + "heading": "モード", + "paragraphs": [ + "モードは参照画像が生成プロセスをどのようにガイドするかを定義します." + ] + }, + "paramSeed": { + "heading": "シード", + "paragraphs": [ + "生成に使用する始動ノイズを制御します.", + "同じ生成設定で同一の結果を生成するには, 「ランダム」オプションを無効にします." + ] + }, + "paramIterations": { + "heading": "生成回数", + "paragraphs": [ + "生成する画像の数。", + "ダイナミックプロンプトが有効の場合、各プロンプトはこの回数生成されます。" + ] + }, + "controlNet": { + "heading": "コントロールネット", + "paragraphs": [ + "コントロールネットは生成を誘導し、構図・レイアウト・構造などが制御された画像の生成に役立ちます。" + ] + }, + "paramWidth": { + "heading": "幅", + "paragraphs": [ + "生成される画像の幅。8の倍数にする必要があります。" + ] + }, + "lora": { + "heading": "LoRA", + "paragraphs": [ + "ベースモデルと組み合わせて使用する軽量モデル." + ] + }, + "loraWeight": { + "heading": "重み", + "paragraphs": [ + "LoRA の重み. 重みを大きくすると, 最終的な画像への影響が大きくなります." + ] + }, + "patchmatchDownScaleSize": { + "heading": "Downscale", + "paragraphs": [ + "埋め込む前にどの程度のダウンスケーリングが行われるか。", + "ダウンスケーリングを大きくするとパフォーマンスは向上しますが、品質は低下します。" + ] + }, + "controlNetWeight": { + "heading": "重み", + "paragraphs": [ + "このレイヤーが生成にどの程度影響を与えるかを調整します", + "• 高い重み (.75-2): 最終結果に大きな影響を及ぼします。", + "• 低い重み (0-.75): 最終結果への影響が小さくなります。" + ] + }, + "paramNegativeConditioning": { + "paragraphs": [ + "生成プロセスでは、ネガティブプロンプトに含まれる概念を回避します.これを使用して、出力から特定の性質やオブジェクトを除外します.", + "強制された構文と埋め込みをサポート." + ], + "heading": "ネガティブプロンプト" + }, + "clipSkip": { + "paragraphs": [ + "スキップする CLIP モデルのレイヤー数.", + "特定のモデルは、CLIP Skip と併用するとより適しています." + ], + "heading": "クリップスキップ" + }, + "compositingMaskBlur": { + "heading": "マスクぼかし", + "paragraphs": [ + "マスクのぼかし半径." + ] + }, + "paramPositiveConditioning": { + "paragraphs": [ + "生成プロセスをガイドします.任意の単語やフレーズを使用できます.", + "強制とダイナミックプロンプト構文と埋め込み。" + ], + "heading": "ポジティブプロンプト" + }, + "compositingMaskAdjustments": { + "heading": "マスク調整", + "paragraphs": [ + "マスクを調整する" + ] + }, + "compositingCoherenceMinDenoise": { + "paragraphs": [ + "境界なじませ処理の最小ノイズ強度", + "インペイント・アウトペイント時の境界なじませ領域の最小ノイズ強度" + ], + "heading": "最小ノイズ除去" + }, + "compositingCoherencePass": { + "paragraphs": [ + "2 回目のノイズ除去は,インペイント/アウトペイントされた画像の合成に役立ちます." + ], + "heading": "境界のなじませ" + }, + "controlNetBeginEnd": { + "paragraphs": [ + "このレイヤーの影響を、ノイズ除去 (生成) 工程のどの範囲に及ぼすかを決めます。", + "• 開始ステップ (%): 生成プロセス内で、このレイヤーからのガイダンスの適用の開始タイミングを指定します。", + "• 終了ステップ (%): このレイヤーのガイダンスの適用を停止し、モデルやその他設定からの一般的な影響のみに戻すタイミングを指定します。" + ], + "heading": "開始/終了ステップの範囲" + }, + "compositingCoherenceEdgeSize": { + "heading": "境界の拡張", + "paragraphs": [ + "なじませ処理の境界拡張サイズ。" + ] + }, + "compositingBlurMethod": { + "paragraphs": [ + "マスクされた領域に適用されるぼかし方法." + ], + "heading": "ぼかし方法" + }, + "inpainting": { + "heading": "インペイント", + "paragraphs": [ + "編集する領域を指定します。" + ] + }, + "dynamicPrompts": { + "heading": "ダイナミックプロンプト", + "paragraphs": [ + "ダイナミック プロンプトは,単一のプロンプトを複数のプロンプトに解析します.", + "基本構文は「{red|green|blue} ball」です。これにより「red ball」「green ball」「blue ball」の3プロンプトが生成されます。", + "1 つのプロンプト内で構文を何度でも使用できますが, 生成されるプロンプトの数を Max Prompts 設定で制限するようにしてください." + ] + }, + "controlNetResizeMode": { + "heading": "リサイズモード", + "paragraphs": [ + "コントロールアダプタの入力画像サイズを出力生成サイズに適合させるメソッド." + ] + }, + "controlNetProcessor": { + "heading": "プロセッサー", + "paragraphs": [ + "入力画像を処理する生成プロセスをガイドするメソッド.プロセッサによって,生成される画像に異なる効果やスタイルが与えられます。" + ] + }, + "controlNetControlMode": { + "heading": "制御モード", + "paragraphs": [ + "プロンプトと コントロールネットの影響度のバランスを調整します。" + ] + }, + "noiseUseCPU": { + "paragraphs": [ + "CPU または GPU でノイズを生成するかどうかを制御します.", + "CPU ノイズを有効にすると, 特定のシードによってどのマシンでも同じ画像が生成されます.", + "CPU ノイズを有効にしてもパフォーマンスに影響はありません." + ], + "heading": "CPUノイズを使用する" + }, + "dynamicPromptsMaxPrompts": { + "heading": "最大プロンプト", + "paragraphs": [ + "ダイナミック プロンプトによって生成できるプロンプトの数を制限します." + ] + }, + "dynamicPromptsSeedBehaviour": { + "paragraphs": [ + "プロンプトを生成するときにシードがどのように使用されるかを制御します.", + "反復ごとに固有のシードを使用します. 単一のシードでプロンプトのバリエーションを試す場合に使用します.", + "たとえば, プロンプトが 5 つある場合, 各画像は同じシードを使用します.", + "「画像ごと」では, 画像ごとに固有のシード値が使用されます. これにより、より多くのバリエーションが得られます." + ], + "heading": "シードの挙動" + }, + "imageFit": { + "paragraphs": [ + "初期画像の幅と高さを出力画像に合わせてサイズ変更します. 有効にすることをお勧めします." + ], + "heading": "初期画像を出力サイズに合わせる" + }, + "infillMethod": { + "heading": "インフィル方法", + "paragraphs": [ + "アウトペインティングまたはインペインティングのプロセス中に埋め込む方法." + ] + }, + "paramGuidance": { + "paragraphs": [ + "プロンプトが生成プロセスにどの程度影響するかを制御します。", + "ガイダンス値が高すぎると過飽和状態になる可能性があり、ガイダンス値が高すぎるか低すぎると生成結果に歪みが生じる可能性があります。ガイダンスはFLUX DEVモデルにのみ適用されます。" + ], + "heading": "ガイダンス" + }, + "paramDenoisingStrength": { + "paragraphs": [ + "生成される画像がラスター レイヤーからどの程度変化するかを制御します。", + "低いほど表示ラスターレイヤーに影響され、高いほどプロンプトに影響されます。", + "生成範囲に可視のラスターレイヤーがない場合、この設定は無視されます。" + ], + "heading": "ノイズ強度" + }, + "refinerStart": { + "heading": "リファイナースタート", + "paragraphs": [ + "生成プロセスのどの時点でリファイナーが使用され始めるか。", + "0 はリファイナーが生成プロセス全体で使用されることを意味し、0.8 は、リファイナーが生成プロセスの最後の 20% で使用されることを意味します。" + ] + }, + "optimizedDenoising": { + "heading": "イメージtoイメージの最適化", + "paragraphs": [ + "「イメージtoイメージを最適化」を有効にすると、Fluxモデルを用いたイメージtoイメージとインペイントで、より段階的なノイズ強度値が適用されます。この設定により、画像の変化量を制御する能力が向上しますが、標準のノイズ強度値を使用したい場合はオフにしてください。この設定は現在調整中のベータ版です。" + ] + }, + "refinerPositiveAestheticScore": { + "heading": "ポジティブ美的スコア", + "paragraphs": [ + "トレーニング データに基づいて、美的スコアの高い画像に類似するように生成を重み付けします。" + ] + }, + "paramCFGScale": { + "paragraphs": [ + "プロンプトが生成プロセスにどの程度影響するかを制御します。", + "CFG スケールの値が高すぎると、飽和しすぎて生成結果が歪む可能性があります。 " + ], + "heading": "CFGスケール" + }, + "paramVAEPrecision": { + "paragraphs": [ + "VAE エンコードおよびデコード時に使用される精度。", + "Fp16/Half 精度は、画像のわずかな変化を犠牲にして、より効率的です。" + ], + "heading": "VAE精度" + }, + "refinerModel": { + "heading": "リファイナーモデル", + "paragraphs": [ + "生成プロセスのリファイナー部分で使用されるモデル。", + "生成モデルに似ています。" + ] + }, + "refinerCfgScale": { + "heading": "CFGスケール", + "paragraphs": [ + "プロンプトが生成プロセスに与える影響を制御する。", + "生成CFG スケールに似ています。" + ] + }, + "seamlessTilingYAxis": { + "heading": "シームレスタイリングY軸", + "paragraphs": [ + "画像を垂直軸に沿ってシームレスに並べます。" + ] + }, + "scaleBeforeProcessing": { + "heading": "生成前のリサイズ", + "paragraphs": [ + "「自動」は、画像生成処理の前に、バウンディングボックスの範囲をモデルに最適なサイズにリサイズします。", + "「手動」は、画像生成処理の前に、バウンディングボックスの範囲をリサイズする幅と高さを選択できます。" + ] + }, + "creativity": { + "heading": "クリエイティビティ", + "paragraphs": [ + "クリエイティビティは、ディテールを追加する際のモデルに与えられる自由度を制御します。クリエイティビティが低いと元のイメージに近いままになり、クリエイティビティが高いとより多くの変化を加えることができます。プロンプトを使用する場合、クリエイティビティが高いとプロンプトの影響が増します。" + ] + }, + "paramHrf": { + "heading": "高解像度修正を有効にする", + "paragraphs": [ + "モデルに最適な解像度よりも高い解像度で、高品質な画像を生成します。通常、生成された画像内の重複を防ぐために使用されます。" + ] + }, + "seamlessTilingXAxis": { + "heading": "シームレスタイリングX軸", + "paragraphs": [ + "画像を水平軸に沿ってシームレスに並べます。" + ] + }, + "paramCFGRescaleMultiplier": { + "paragraphs": [ + "ゼロ端末 SNR (ztsnr) を使用してトレーニングされたモデルに使用される、CFG ガイダンスのリスケールマルチプライヤー。", + "これらのモデルの場合、推奨値は 0.7 です。" + ], + "heading": "CFG リスケールマルチプライヤー" + }, + "structure": { + "heading": "ストラクチャ", + "paragraphs": [ + "ストラクチャは、出力画像が元のレイアウトにどれだけ忠実に従うかを制御します。低いストラクチャでは大幅な変更が可能ですが、高いストラクチャでは元の構成とレイアウトが厳密に維持されます。" + ] + }, + "refinerNegativeAestheticScore": { + "paragraphs": [ + "トレーニング データに基づいて、美観スコアが低い画像に類似するように生成に重み付けします。" + ], + "heading": "ネガティブ美的スコア" + }, + "fluxDevLicense": { + "heading": "非商用ライセンス", + "paragraphs": [ + "FLUX.1 [dev]モデルは、FLUX [dev]非商用ライセンスに基づいてライセンスされています。Invokeでこのモデルタイプを商用目的で使用する場合は、当社のウェブサイトをご覧ください。" + ] + }, + "tileSize": { + "heading": "タイルサイズ", + "paragraphs": [ + "アップスケール処理で使用するタイルのサイズを制御します。タイルのサイズが大きいほどメモリ消費量は多くなりますが、より良い結果が得られる可能性があります。", + "SD1.5 モデルのデフォルトは 768 ですが、SDXL モデルのデフォルトは 1024 です。メモリの問題が発生した場合は、タイルのサイズを小さくしてください。" + ] + }, + "tileOverlap": { + "heading": "タイルオーバーラップ", + "paragraphs": [ + "アップスケール時の隣接するタイルの重なり具合を制御します。重なり具合の値を大きくするとタイル間の継ぎ目が見えにくくなりますが、メモリ使用量は増加します。", + "デフォルト値の 128 はほとんどの場合に適していますが、特定のニーズやメモリの制約に基づいて調整できます。" + ] + }, + "colorCompensation": { + "heading": "色補正", + "paragraphs": [ + "入力画像を調整し、インペイントや img2imgによる色の変化を減らします(SDXL限定) 。" + ] + } + }, + "accordions": { + "compositing": { + "infillTab": "インフィル", + "title": "コンポジット", + "coherenceTab": "境界のなじませ" + }, + "advanced": { + "title": "高度", + "options": "$t(accordions.advanced.title) オプション" + }, + "control": { + "title": "コントロール" + }, + "generation": { + "title": "生成" + }, + "image": { + "title": "画像" + } + }, + "hrf": { + "metadata": { + "method": "高解像修復の手法", + "strength": "高解像修復の強度", + "enabled": "高解像修復が有効" + }, + "hrf": "高解像修復", + "enableHrf": "高解像度修正を有効にする", + "upscaleMethod": "アップスケール手法" + }, + "prompt": { + "addPromptTrigger": "トリガーワードを追加", + "compatibleEmbeddings": "互換性のある埋め込み", + "noMatchingTriggers": "一致するトリガーがありません", + "generateFromImage": "画像からプロンプトを生成する", + "expandCurrentPrompt": "現在のプロンプトを拡張", + "uploadImageForPromptGeneration": "プロンプト生成用の画像をアップロードする", + "expandingPrompt": "プロンプトを拡張中...", + "replace": "置換する", + "discard": "破棄する", + "resultTitle": "プロンプト拡張完了", + "resultSubtitle": "拡張プロンプトの処理方法を選択します:", + "insert": "挿入する", + "noPromptHistory": "プロンプトヒストリーが記録されていません。", + "noMatchingPrompts": "一致するプロンプトがヒストリーにありません。", + "toSwitchBetweenPrompts": "プロンプトを切り替えます。", + "promptHistory": "プロンプトヒストリー", + "clearHistory": "ヒストリーをクリア", + "usePrompt": "プロンプトを使用", + "searchPrompts": "検索...", + "expandPromptWithLLM": "プロンプトをLLMで拡張", + "expandPrompt": "プロンプトを拡張", + "expand": "拡張する", + "openModelManager": "モデルマネージャーを開く", + "imageToPrompt": "画像からプロンプト生成", + "selectVisionModel": "画像認識モデルを選択...", + "changeImage": "画像を変更", + "uploadImage": "画像をアップロード", + "generatePrompt": "プロンプトを生成", + "selectTextLLM": "LLMを選択...", + "noTextLLMInstalledTitle": "LLMがインストールされていません", + "noVisionModelInstalledTitle": "画像認識モデルがインストールされていません", + "noTextLLMInstalledDescription": "プロンプト拡張にはLLMが必要です。推奨モデルは軽量・高速な Qwen2.5-1.5B-Instruct (~3 GB) です。スターターモデルから追加できます。", + "noVisionModelInstalledDescription": "画像からプロンプト生成には画像認識モデル(LLaVA Onevisionなど)が必要です。0.5B(~1 GB)は軽量デフォルトです。" + }, + "ui": { + "tabs": { + "queue": "キュー", + "canvas": "キャンバス", + "workflows": "ワークフロー", + "models": "モデル", + "gallery": "ギャラリー", + "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)", + "modelsTab": "$t(ui.tabs.models) $t(common.tab)", + "upscaling": "アップスケール", + "upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)", + "generate": "生成", + "customNodes": "ノード" + }, + "launchpad": { + "upscaling": { + "model": "モデル", + "scale": "スケール", + "helpText": { + "promptAdvice": "アップスケールする際は、媒体とスタイルを説明するプロンプトを使用してください。画像内の具体的なコンテンツの詳細を説明することは避けてください。", + "styleAdvice": "アップスケールは、画像の全体的なスタイルに最適です。" + }, + "uploadImage": { + "title": "アップスケール用の画像をアップロードする", + "description": "ここをクリック、または画像をドラッグしてください(JPG、PNG、WebP、最大100MB)" + }, + "replaceImage": { + "title": "現在の画像を置き換える", + "description": "新しい画像をクリックまたはドラッグして、現在の画像を置き換えます" + }, + "imageReady": { + "title": "画像準備完了", + "description": "アップスケールを開始するにはInvokeを押してください" + }, + "readyToUpscale": { + "title": "アップスケールの準備ができました!", + "description": "以下の設定を構成し、「Invoke」ボタンをクリックして画像のアップスケールを開始します。" + }, + "upscaleModel": "アップスケールモデル", + "creativityAndStructure": { + "title": "創造性と構造のデフォルト", + "conservative": "保守的", + "balanced": "バランス", + "creative": "クリエイティブ", + "artistic": "芸術的" + } + }, + "workflowsTitle": "ワークフローを詳しく見てみましょう。", + "upscalingTitle": "アップスケールしてディテールを追加。", + "canvasTitle": "キャンバスで編集・調整。", + "generateTitle": "プロンプトから画像を生成。", + "modelGuideText": "モデル毎に最適なプロンプトとは?", + "modelGuideLink": "モデルガイドをご覧ください。", + "workflows": { + "description": "ワークフローは、画像生成を自動化する再利用可能な仕組みです。複雑な編集や大量の処理を自動で実行することができます。", + "learnMoreLink": "ワークフローの作成について詳しく見る", + "browseTemplates": { + "title": "ワークフローテンプレートを参照する", + "description": "一般的なタスク用のプリセットワークフローから選択" + }, + "createNew": { + "title": "新規ワークフローを作成", + "description": "新しいワークフローを自分で作る" + }, + "loadFromFile": { + "title": "ファイルからワークフローを読み込む", + "description": "既存のワークフローをアップロードして開始する" + } + }, + "createNewWorkflowFromScratch": "新しいワークフローを最初から作成する", + "browseAndLoadWorkflows": "既存のワークフローを参照して読み込む", + "addStyleRef": { + "title": "スタイル参照を追加する", + "description": "リファレンス用画像を追加。" + }, + "editImage": { + "title": "画像を編集", + "description": "リファインする画像を追加する。" + }, + "generateFromText": { + "title": "テキストから生成", + "description": "プロンプトを入力して生成する。" + }, + "useALayoutImage": { + "title": "レイアウト画像を使用", + "description": "構図や形状を制御するための画像を追加する。" + }, + "generate": { + "canvasCalloutTitle": "画像をさらに細かく制御、編集、改善するには?", + "canvasCalloutLink": "Canvas モードで最高の制御性と自由度を。" + } + }, + "panels": { + "launchpad": "ローンチパッド", + "workflowEditor": "ワークフローエディター", + "imageViewer": "ビューア", + "canvas": "キャンバス" + } + }, + "controlLayers": { + "regionalReferenceImage": "領域参照画像", + "saveLayerToAssets": "レイヤーをアセットに保存", + "global": "全域", + "opacity": "透明度", + "canvasContextMenu": { + "newRegionalGuidance": "新規領域ガイダンス", + "bboxGroup": "バウンディングボックスから作成", + "cropCanvasToBbox": "キャンバスをバウンディングボックスでクロップ", + "newGlobalReferenceImage": "新規全域参照画像", + "newRegionalReferenceImage": "新規領域参照画像", + "canvasGroup": "キャンバス", + "saveToGalleryGroup": "ギャラリーに保存", + "saveCanvasToGallery": "キャンバスをギャラリーに保存", + "saveBboxToGallery": "バウンディングボックスをギャラリーに保存", + "newControlLayer": "新規コントロールレイヤー", + "newRasterLayer": "新規ラスターレイヤー", + "newInpaintMask": "新規インペイントマスク", + "copyToClipboard": "クリップボードにコピー", + "copyCanvasToClipboard": "キャンバスをクリップボードにコピー", + "copyBboxToClipboard": "バウンディングボックスをクリップボードにコピー", + "newResizedControlLayer": "新規コントロールレイヤー(リサイズ)" + }, + "regionalGuidance": "領域ガイダンス", + "globalReferenceImage": "全域参照画像", + "moveForward": "前面へ移動", + "copyInpaintMaskTo": "$t(controlLayers.inpaintMask) をコピー", + "transform": { + "fitToBbox": "バウンディングボックスにフィット", + "transform": "変形", + "apply": "適用", + "cancel": "キャンセル", + "reset": "リセット", + "fitMode": "フィットモード", + "fitModeContain": "含む", + "fitModeCover": "覆う", + "fitModeFill": "満たす", + "smoothing": "スムージング", + "smoothingDesc": "変形を確定する際に、高品質なリサンプル処理を行います。", + "smoothingMode": "再サンプル", + "smoothingModeBilinear": "バイリニア", + "smoothingModeBicubic": "バイキュービック", + "smoothingModeHamming": "ハミング", + "smoothingModeLanczos": "ランチョス" + }, + "cropLayerToBbox": "レイヤーをバウンディングボックスでクロップ", + "convertInpaintMaskTo": "$t(controlLayers.inpaintMask)を変換", + "regionalGuidance_withCount_other": "領域ガイダンス", + "tool": { + "colorPicker": "スポイト", + "brush": "ブラシ", + "rectangle": "矩形", + "move": "移動", + "eraser": "消しゴム", + "bbox": "バウンディングボックス", + "view": "ビュー", + "shapes": "シェイプ", + "lasso": "投げ縄", + "gradient": "グラデーション", + "text": "テキスト" + }, + "saveCanvasToGallery": "キャンバスをギャラリーに保存", + "saveBboxToGallery": "バウンディングボックスをギャラリーへ保存", + "moveToBack": "最背面へ移動", + "duplicate": "複製", + "addLayer": "レイヤーを追加", + "rasterLayer": "ラスターレイヤー", + "regional": "領域", + "rectangle": "矩形", + "moveBackward": "背面へ移動", + "moveToFront": "最前面へ移動", + "mergeDown": "下のレイヤーと結合", + "inpaintMask_withCount_other": "インペイントマスク", + "canvas": "キャンバス", + "fitBboxToLayers": "バウンディングボックスを表示レイヤーにフィット", + "removeBookmark": "ブックマークを外す", + "savedToGalleryOk": "ギャラリーに保存しました", + "controlMode": { + "prompt": "プロンプト", + "controlMode": "制御モード", + "balanced": "バランス(推奨)", + "control": "コントロール", + "megaControl": "メガコントロール" + }, + "prompt": "プロンプト", + "settings": { + "snapToGrid": { + "off": "オフ", + "on": "オン", + "label": "グリッドにスナップ" + }, + "preserveMask": { + "label": "マスクされた領域を保護", + "alert": "マスクされた領域を保護中" + }, + "isolatedStagingPreview": "分離されたステージングプレビュー", + "isolatedPreview": "単体プレビュー", + "isolatedLayerPreview": "分離されたレイヤーのプレビュー", + "isolatedLayerPreviewDesc": "フィルタリングや変換などの操作を実行するときに、このレイヤーのみを表示するかどうか。", + "invertBrushSizeScrollDirection": "ブラシサイズ調整のスクロール方向を反転", + "pressureSensitivity": "筆圧感知", + "saveAllImagesToGallery": { + "label": "ギャラリーに新しい生成画像を送る", + "alert": "キャンバスを経由せず、直接ギャラリーに生成画像が送られます" + } + }, + "filter": { + "filter": "フィルター", + "spandrel_filter": { + "model": "モデル", + "label": "img2imgモデル", + "description": "選択したレイヤーでimg2imgモデルを実行します。", + "autoScale": "オートスケール", + "autoScaleDesc": "選択したモデルは、目標スケールに達するまで実行されます。", + "scale": "ターゲットスケール" + }, + "apply": "適用", + "reset": "リセット", + "cancel": "キャンセル", + "filters": "フィルター", + "filterType": "フィルタータイプ", + "autoProcess": "自動で実行", + "process": "処理", + "advanced": "詳細設定", + "processingLayerWith": "{{type}} フィルターを使用した処理レイヤー。", + "forMoreControl": "さらに細かく制御するには、以下の「詳細設定」をクリックしてください。", + "canny_edge_detection": { + "label": "エッジ検出(Canny)", + "description": "Canny エッジ検出アルゴリズムを使用して、選択したレイヤーから線画を生成します。", + "low_threshold": "低閾値", + "high_threshold": "高閾値" + }, + "color_map": { + "label": "カラーマップ", + "description": "選択したレイヤーからカラーマップを作成します。", + "tile_size": "タイルサイズ" + }, + "content_shuffle": { + "label": "コンテンツシャッフル", + "description": "選択したレイヤーのコンテンツを、「液化」効果と同様にシャッフルします。", + "scale_factor": "スケール係数" + }, + "depth_anything_depth_estimation": { + "label": "深度抽出(Depth Anything)", + "description": "Depth Anthingモデルを使用して、選択したレイヤーから深度マップを生成します。", + "model_size": "モデルサイズ", + "model_size_small": "スモール", + "model_size_small_v2": "スモールv2", + "model_size_base": "ベース", + "model_size_large": "ラージ" + }, + "dw_openpose_detection": { + "label": "ポーズ検出(DW Openpose)", + "description": "DW Openpose モデルを使用して、選択したレイヤー内の人間のポーズを検出します。", + "draw_hands": "手を描く", + "draw_face": "顔を描く", + "draw_body": "体を描く" + }, + "hed_edge_detection": { + "label": "エッジ検出(HED)", + "description": "HED エッジ検出モデルを使用して、選択したレイヤーから線画を生成します。", + "scribble": "落書き" + }, + "lineart_anime_edge_detection": { + "label": "エッジ検出(Lineart Anime)", + "description": "Lineart Animeエッジ検出モデルを使用して、選択したレイヤーから線画を生成します。" + }, + "lineart_edge_detection": { + "label": "エッジ検出(Lineart)", + "description": "Linartエッジ検出モデルを使用して、選択したレイヤーから線画を生成します。", + "coarse": "粗く" + }, + "mediapipe_face_detection": { + "label": "顔検出(MediaPipe)", + "description": "MediaPipe顔検出モデルを使用して、選択したレイヤー内の顔を検出します。", + "max_faces": "最大顔数", + "min_confidence": "最小信頼度" + }, + "mlsd_detection": { + "label": "直線検出(MLSD)", + "description": "MLSD 線分検出モデルを使用して、選択したレイヤーから直線部分を抽出します。", + "score_threshold": "スコア閾値", + "distance_threshold": "距離閾値" + }, + "normal_map": { + "label": "ノーマルマップ推定", + "description": "選択したレイヤーからノーマルマップを生成します。" + }, + "pidi_edge_detection": { + "label": "エッジ検出(PiDiNet)", + "description": "PiDiNet エッジ検出モデルを使用して、選択したレイヤーから線画を生成します。", + "scribble": "落書き", + "quantize_edges": "エッジを量子化する" + }, + "img_blur": { + "label": "ぼかし", + "description": "選択したレイヤーをぼかします。", + "blur_type": "ぼかしタイプ", + "blur_radius": "半径", + "gaussian_type": "ガウス", + "box_type": "ボックス" + }, + "img_noise": { + "label": "ノイズ", + "description": "選択したレイヤーにノイズを追加します。", + "noise_type": "ノイズの種類", + "noise_amount": "量", + "gaussian_type": "ガウス", + "salt_and_pepper_type": "インパルス", + "noise_color": "カラーノイズ", + "size": "ノイズサイズ" + }, + "adjust_image": { + "label": "色調補正", + "description": "画像の選択したチャンネルを調整します。", + "channel": "チャンネル", + "value_setting": "量", + "scale_values": "スケール値", + "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": "A(ラボ)", + "b": "B(ラボ)", + "y": "Y(YCbCr)", + "cb": "Cb(YCbCr)", + "cr": "Cr(YCbCr)" + }, + "pbr_maps": { + "label": "PBRマップを作成" + } + }, + "weight": "重み", + "bookmark": "クイックスイッチのブックマーク", + "exportCanvasToPSD": "キャンバスをPSDにエクスポート", + "savedToGalleryError": "ギャラリーへの保存中にエラーが発生しました", + "regionCopiedToClipboard": "{{region}} をクリップボードにコピーしました", + "copyRegionError": "{{region}} のコピー中にエラーが発生しました", + "newGlobalReferenceImageOk": "作成されたグローバル参照画像", + "newGlobalReferenceImageError": "グローバル参照イメージの作成中に問題が発生しました", + "newRegionalReferenceImageOk": "領域参照画像の作成", + "newRegionalReferenceImageError": "領域参照画像の作成中に問題が発生しました", + "newControlLayerOk": "作成されたコントロールレイヤー", + "newControlLayerError": "制御層の作成中に問題が発生しました", + "newRasterLayerOk": "ラスターレイヤーを作成しました", + "newRasterLayerError": "ラスターレイヤーの作成中に問題が発生しました", + "pullBboxIntoLayerOk": "バウンディングボックスをレイヤーに", + "pullBboxIntoLayerError": "バウンディングボックスをレイヤーにする際に問題が発生しました", + "pullBboxIntoReferenceImageOk": "バウンディングボックスが参照画像にされました", + "pullBboxIntoReferenceImageError": "バウンディングボックスを参照画像にする際に問題が発生しました", + "regionIsEmpty": "選択した領域は空です", + "mergeVisible": "可視を統合した新レイヤー", + "mergeVisibleOk": "レイヤーが結合されました", + "mergeVisibleError": "レイヤーの結合エラー", + "mergingLayers": "レイヤーの結合中", + "clearHistory": "ヒストリーをクリア", + "bboxOverlay": "バウンディングボックス外を暗くする", + "ruleOfThirds": "三分割法を表示", + "newSession": "新しいセッション", + "clearCaches": "キャッシュをクリア", + "recalculateRects": "矩形を再計算する", + "clipToBbox": "描画をバウンディングボックス内に制限", + "outputOnlyMaskedRegions": "生成された領域のみを出力する", + "width": "幅", + "autoNegative": "オートネガティブ", + "enableAutoNegative": "オートネガティブを有効にする", + "disableAutoNegative": "オートネガティブを無効にする", + "deleteReferenceImage": "参照画像を削除", + "showHUD": "HUDを表示", + "maskFill": "マスク色", + "addPositivePrompt": "$t(controlLayers.prompt)を追加", + "addNegativePrompt": "$t(controlLayers.negativePrompt)を追加", + "addReferenceImage": "$t(controlLayers.referenceImage)を追加", + "addImageNoise": "$t(controlLayers.imageNoise)を追加します", + "addRasterLayer": "$t(controlLayers.rasterLayer)を追加します", + "addControlLayer": "$t(controlLayers.controlLayer)を追加します", + "addInpaintMask": "$t(controlLayers.inpaintMask)を追加します", + "addRegionalGuidance": "$t(controlLayers.regionalGuidance)を追加します", + "addDenoiseLimit": "$t(controlLayers.denoiseLimit)を追加します", + "controlLayer": "コントロールレイヤー", + "inpaintMask": "インペイントマスク", + "referenceImageRegional": "参考画像(領域)", + "asRasterLayer": "$t(controlLayers.rasterLayer) として", + "asRasterLayerResize": "$t(controlLayers.rasterLayer) として (リサイズ)", + "asControlLayer": "$t(controlLayers.controlLayer) として", + "asControlLayerResize": "$t(controlLayers.controlLayer) として (リサイズ)", + "referenceImage": "参照画像", + "sendToCanvas": "キャンバスに送る", + "newLayerFromImage": "画像から新規レイヤー", + "newCanvasFromImage": "画像から新規キャンバス", + "copyToClipboard": "クリップボードにコピー", + "rasterLayer_withCount_other": "ラスターレイヤー", + "controlLayer_withCount_other": "コントロールレイヤー", + "layer_other": "レイヤー", + "convertRasterLayerTo": "$t(controlLayers.rasterLayer) を変換する", + "convertControlLayerTo": "$t(controlLayers.controlLayer) を変換する", + "convertRegionalGuidanceTo": "$t(controlLayers.regionalGuidance) を変換する", + "copyRasterLayerTo": "$t(controlLayers.rasterLayer)をコピーする", + "copyControlLayerTo": "$t(controlLayers.controlLayer) をコピーする", + "copyRegionalGuidanceTo": "$t(controlLayers.regionalGuidance)をコピーする", + "newRasterLayer": "新しい $t(controlLayers.rasterLayer)", + "newControlLayer": "新しい $t(controlLayers.controlLayer)", + "newInpaintMask": "新しい $t(controlLayers.inpaintMask)", + "newRegionalGuidance": "新しい $t(controlLayers.regionalGuidance)", + "pasteTo": "貼り付け先", + "pasteToAssets": "アセット", + "pasteToAssetsDesc": "アセットに貼り付け", + "pasteToBbox": "バウンディングボックス", + "pasteToBboxDesc": "新しいレイヤー(バウンディングボックス内)", + "pasteToCanvas": "キャンバス", + "pasteToCanvasDesc": "新しいレイヤー(キャンバス内)", + "transparency": "透過表示", + "enableTransparencyEffect": "透過表示を有効にする", + "disableTransparencyEffect": "透過表示を無効にする", + "hidingType": "{{type}} を非表示", + "showingType": "{{type}}を表示", + "showNonRasterLayers": "非ラスターレイヤーを表示 (Shift+H)", + "hideNonRasterLayers": "非ラスターレイヤーを非表示にする (Shift+H)", + "dynamicGrid": "ダイナミックグリッド", + "logDebugInfo": "デバッグ情報をログに記録する", + "locked": "ロックされています", + "unlocked": "ロック解除", + "deleteSelected": "選択項目を削除", + "replaceLayer": "レイヤーの置き換え", + "pullBboxIntoLayer": "バウンディングボックスをレイヤーに", + "pullBboxIntoReferenceImage": "バウンディングボックスを参照画像に", + "showProgressOnCanvas": "キャンバスに進捗状況を表示", + "useImage": "画像を使う", + "negativePrompt": "ネガティブプロンプト", + "beginEndStepPercentShort": "開始/終了 %", + "resetCanvasLayers": "キャンバスとレイヤーをリセット", + "resetGenerationSettings": "生成設定をリセット", + "controlLayerEmptyState": "画像をアップロード、ギャラリーからこのレイヤーに画像をドラッグ、バウンディングボックスをこのレイヤーにする、またはキャンバスに描画して開始します。", + "referenceImageEmptyStateWithCanvasOptions": "画像をアップロード、またはギャラリーからここに画像をドラッグ、あるいはバウンディングボックス範囲を参照画像にします。", + "referenceImageEmptyState": "画像をアップロードするか、ギャラリーからこの参照画像に画像をドラッグします。", + "imageNoise": "画像ノイズ", + "denoiseLimit": "ノイズ除去制限", + "warnings": { + "problemsFound": "問題が見つかりました", + "unsupportedModel": "選択したベースモデルではレイヤーがサポートされていません", + "controlAdapterNoModelSelected": "コントロールレイヤーのモデルが選択されていません", + "controlAdapterIncompatibleBaseModel": "コントロールレイヤーのベースモデルに互換性がありません", + "controlAdapterNoControl": "コントロールが選択/描画されていません", + "ipAdapterNoModelSelected": "参照画像モデルが選択されていません", + "ipAdapterIncompatibleBaseModel": "互換性のない参照画像ベースモデル", + "ipAdapterNoImageSelected": "参照画像が選択されていません", + "rgNoPromptsOrIPAdapters": "テキストプロンプトや参照画像がありません", + "rgNegativePromptNotSupported": "選択されたベースモデルでは否定プロンプトはサポートされていません", + "rgReferenceImagesNotSupported": "選択されたベースモデルでは領域参照画像はサポートされていません", + "rgAutoNegativeNotSupported": "選択したベースモデルでは自動否定はサポートされていません", + "rgNoRegion": "領域が描画されていません", + "fluxFillIncompatibleWithControlLoRA": "コントロールLoRAはFLUX Fillと互換性がありません", + "bboxHidden": "バウンディングボックスは非表示です(Shift+O で切り替え)" + }, + "errors": { + "unableToFindImage": "画像が見つかりません", + "unableToLoadImage": "画像を読み込めません" + }, + "ipAdapterMethod": { + "ipAdapterMethod": "モード", + "full": "スタイルと構成", + "fullDesc": "視覚スタイル (色、テクスチャ) と構成 (レイアウト、構造) を適用します。", + "style": "スタイル(シンプル)", + "styleDesc": "レイアウトを考慮せずに視覚スタイル(色、テクスチャ)を適用します。以前は「スタイルのみ」と呼ばれていました。", + "composition": "構成のみ", + "compositionDesc": "参照スタイルを無視してレイアウトと構造を複製します。", + "styleStrong": "スタイル(強力)", + "styleStrongDesc": "構成への影響をわずかに抑えて、強力なビジュアル スタイルを適用します。", + "stylePrecise": "スタイル(正確)", + "stylePreciseDesc": "被写体の影響を排除し、正確な視覚スタイルを適用します。" + }, + "fluxReduxImageInfluence": { + "imageInfluence": "イメージの影響力", + "lowest": "最低", + "low": "低", + "medium": "中", + "high": "高", + "highest": "最高" + }, + "fill": { + "fillColor": "描画色", + "fillStyle": "表示スタイル", + "solid": "ソリッド", + "grid": "グリッド", + "crosshatch": "クロスハッチ", + "vertical": "垂直", + "horizontal": "水平", + "diagonal": "対角線", + "bgFillColor": "サブカラー", + "fgFillColor": "メインカラー", + "switchColors": "メインカラー/サブカラーの切り替え(X)" + }, + "selectObject": { + "selectObject": "オブジェクトを選択", + "pointType": "点タイプ", + "invertSelection": "選択範囲を反転", + "include": "含む", + "exclude": "除外", + "neutral": "ニュートラル", + "apply": "適用", + "reset": "リセット", + "saveAs": "保存", + "cancel": "キャンセル", + "process": "処理", + "clickToAdd": "レイヤーをクリックしてポイントを追加します", + "dragToMove": "ポイントをドラッグして移動します", + "clickToRemove": "ポイントをクリックして削除します", + "desc": "対象オブジェクトを1つ選択します。選択が完了したら、適用 をクリックして選択範囲外のすべてを削除するか、選択範囲を新しいレイヤーとして保存します。", + "visualModeDesc": "ビジュアル モードでは、ボックスと点の入力を使用してオブジェクトを選択します。", + "visualMode1": "クリック&ドラッグして、選択したいオブジェクトの周囲にボックスを描きます。オブジェクトより少し大きいか小さいボックスを描くと、より良い結果が得られる場合があります。", + "visualMode2": "クリックして緑の include ポイントを追加するか、Shift キーを押しながらクリックして赤の exclude ポイントを追加し、モデルに含める内容と除外する内容を指示します。", + "visualMode3": "ポイントは、ボックスの選択を絞り込むために使用することも、独立して使用することもできます。", + "promptModeDesc": "プロンプト モードでは、テキスト入力を使用してオブジェクトを選択します。", + "promptMode1": "選択するオブジェクトの簡単な説明を入力します。", + "promptMode2": "複雑な説明や複数のオブジェクトを避け、簡単な言葉を使用してください。", + "model": "モデル", + "segmentAnything1": "Segment Anything 1", + "segmentAnything2": "Segment Anything 2", + "prompt": "選択プロンプト" + }, + "HUD": { + "bbox": "バウンディングボックス", + "scaledBbox": "リサイズ後のバウンディングボックス", + "entityStatus": { + "isFiltering": "{{title}} はフィルタリング中です", + "isTransforming": "{{title}}を変形中です", + "isLocked": "{{title}}はロックされています", + "isHidden": "{{title}}は非表示になっています", + "isDisabled": "{{title}}は無効です", + "isEmpty": "{{title}} は空です" + }, + "textSessionActive": "文字入力がアクティブ中" + }, + "stagingArea": { + "accept": "採用", + "discardAll": "すべて破棄", + "discard": "破棄", + "previous": "前へ", + "next": "次へ", + "saveToGallery": "ギャラリーに保存", + "showResultsOn": "結果を非表示にする", + "showResultsOff": "結果を表示する", + "hideThumbnails": "サムネイルを非表示", + "showThumbnails": "サムネイルを表示" + }, + "fitBboxToMasks": "バウンディングボックスをマスクにフィットさせる", + "addAdjustments": "調整を追加", + "removeAdjustments": "調整を削除", + "adjustments": { + "simple": "シンプル", + "curves": "カーブ", + "heading": "調整", + "expand": "調整を展開", + "collapse": "調整を閉じる", + "brightness": "輝度", + "contrast": "コントラスト", + "saturation": "彩度", + "temperature": "色温度", + "tint": "色相", + "sharpness": "シャープ", + "finish": "終了", + "reset": "リセット", + "master": "マスター" + }, + "deletePrompt": "プロンプトを削除", + "addGlobalReferenceImage": "$t(controlLayers.globalReferenceImage) を追加します", + "invertMask": "マスクを反転", + "referenceImageGlobal": "参考画像(グローバル)", + "maxRefImages": "最大参照画像", + "useAsReferenceImage": "参照画像として使う", + "sendingToCanvas": "キャンバスに生成をステージングする", + "sendingToGallery": "生成をギャラリーに送る", + "sendToGallery": "ギャラリーに送る", + "sendToGalleryDesc": "「Invoke」を押すと固有の画像が生成され、ギャラリーに保存されます。", + "newImg2ImgCanvasFromImage": "新しい img2img からの画像", + "sendToCanvasDesc": "「Invoke」を押すと、進行中の作業がキャンバス上でステージングされます。", + "viewProgressInViewer": "画像ビューアで進行状況と出力を表示します。", + "viewProgressOnCanvas": "キャンバス で進行状況とステージ出力を表示します。", + "globalReferenceImage_withCount_other": "グローバル参照画像", + "regionalGuidance_withCount_hidden": "領域ガイダンス({{count}}件非表示)", + "controlLayers_withCount_hidden": "コントロールレイヤー({{count}} 個非表示)", + "rasterLayers_withCount_hidden": "ラスターレイヤー ({{count}} 個非表示)", + "globalReferenceImages_withCount_hidden": "グローバル参照画像({{count}} 枚非表示)", + "inpaintMasks_withCount_hidden": "インペイントマスク({{count}} 個非表示)", + "regionalGuidance_withCount_visible": "領域ガイダンス ({{count}})", + "controlLayers_withCount_visible": "コントロールレイヤー ({{count}})", + "rasterLayers_withCount_visible": "ラスターレイヤー({{count}})", + "globalReferenceImages_withCount_visible": "グローバル参照画像 ({{count}})", + "inpaintMasks_withCount_visible": "インペイントマスク({{count}})", + "layer_withCount_other": "レイヤー数 ({{count}})", + "pastedTo": "{{destination}} に貼り付けました", + "stagingOnCanvas": "ステージング画像", + "newGallerySession": "新しいギャラリーセッション", + "newGallerySessionDesc": "これにより、キャンバスとモデル選択以外のすべての設定がクリアされます。生成された画像はギャラリーに送信されます。", + "newCanvasSession": "新しいキャンバスセッション", + "newCanvasSessionDesc": "これにより、キャンバスとモデル選択以外のすべての設定がクリアされます。生成はキャンバス上でステージングされます。", + "replaceCurrent": "現在のものを置き換える", + "uploadOrDragAnImage": "ギャラリーから画像をドラッグするか、画像をアップロードします。", + "autoSwitch": { + "off": "オフ", + "switchOnStart": "生成開始時", + "switchOnFinish": "生成完了時", + "doNotAutoSwitch": "自動切り替えをしない", + "switchOnStartDesc": "生成開始時に切り替え", + "switchOnFinishDesc": "生成完了時に切り替え" + }, + "extractRegion": "領域を抽出", + "gradient": { + "linear": "線形", + "radial": "円形", + "clip": "グラデーションをドラッグ範囲に限定" + }, + "lasso": { + "polygon": "多角形", + "freehand": "フリーハンド", + "polygonHint": "クリックで頂点を追加、最初の頂点をクリックで閉じます。" + }, + "canvasProject": { + "project": "プロジェクト", + "saveProject": "キャンバスをプロジェクトファイルに保存", + "loadProject": "プロジェクトファイルをロード", + "saveSuccess": "プロジェクトファイルが保存されました", + "saveSuccessDesc": "{{count}} 枚の画像を含むプロジェクトファイルが保存されました", + "saveError": "プロジェクトファイルの保存に失敗しました", + "loadSuccess": "プロジェクトがロードされました", + "loadSuccessDesc": "プロジェクトファイルからキャンバスの状態が復元されました", + "loadError": "プロジェクトファイルの読み込みに失敗しました", + "loadWarning": "プロジェクトをロードすると、全てのレイヤー、マスク、参照画像、生成パラメータを含む現在のキャンバス状態が置換されます。このアクションは取り消しできません。", + "projectName": "プロジェクト名" + }, + "snapshot": { + "snapshots": "キャンバスのスナップショットの保存と読み込み", + "saveSnapshot": "スナップショットの保存", + "restoreSnapshot": "スナップショットの復元", + "snapshotNamePlaceholder": "スナップショットの名前", + "save": "保存", + "delete": "削除", + "snapshotSaved": "スナップショット \"{{name}}\" が保存されました", + "snapshotRestored": "スナップショット \"{{name}}\" が復元されました", + "snapshotDeleted": "スナップショット \"{{name}}\" が削除されました", + "snapshotSaveFailed": "スナップショットの保存に失敗しました", + "snapshotRestoreFailed": "スナップショットの復元に失敗しました", + "snapshotDeleteFailed": "スナップショットの削除に失敗しました", + "snapshotMissingImages_other": "このスナップショットから参照されている{{count}} 枚の画像が存在しないため、プレースホルダーとして表示されます", + "snapshotIncompatible": "このスナップショットは異なるバージョンで制作されているため、互換性がありません", + "overwriteSnapshotTitle": "スナップショットを上書きしますか?", + "overwriteSnapshotMessage": "\"{{name}}\" というスナップショットはすでに存在します。上書きしますか?", + "overwrite": "上書き" + }, + "compositeOperation": { + "label": "合成モード", + "add": "合成モードを追加", + "remove": "合成モードを削除", + "blendModes": { + "source-over": "通常", + "color": "カラー", + "hue": "色相", + "overlay": "オーバーレイ", + "soft-light": "ソフトライト", + "hard-light": "ハードライト", + "screen": "スクリーン", + "color-burn": "焼き込みカラー", + "color-dodge": "覆い焼きカラー", + "multiply": "乗算", + "darken": "比較(暗)", + "lighten": "比較(明)", + "difference": "差の絶対値", + "luminosity": "輝度", + "saturation": "彩度" + } + }, + "transparencyLocked": "透明ピクセルの保護が有効", + "transparencyUnlocked": "透明ピクセルの保護が無効", + "booleanOps": { + "label": "ブール演算", + "intersect": "交差", + "cutout": "下を型抜き", + "exclude": "中マド", + "cutaway": "下で型抜き" + }, + "disableReferenceImage": "参照画像を無効化", + "enableReferenceImage": "参照画像を有効化", + "maskLayerEmpty": "マスクレイヤーが空です", + "extractMaskedAreaFailed": "マスク領域の抽出ができません。", + "extractMaskedAreaMissingData": "抽出ができません:画像かマスクデータが不明です。", + "invertRegion": "領域反転", + "workflowIntegration": { + "title": "キャンバスでワークフローを使う", + "runWorkflow": "ワークフローを使う", + "execute": "ワークフローを実行", + "executing": "実行中..." + }, + "shape": { + "rect": "矩形", + "oval": "楕円" + }, + "modifierHints": { + "labels": { + "pan": "パン", + "moveShape": "シェイプを移動", + "pickColor": "色をスポイト", + "straightLine": "直線", + "resizeBrush": "ブラシサイズの変更", + "resizeEraser": "消しゴムサイズの変更", + "erase": "消去", + "snap45Degrees": "45度でスナップ", + "lockAspectRatio": "縦横比を固定", + "unlockAspectRatio": "縦横比の固定を解除", + "scaleFromCenter": "中央から変形", + "snapRotation": "回転をスナップ", + "nudgeSelection": "選択をナッジ", + "cancelText": "キャンセル" + } + } + }, + "stylePresets": { + "clearTemplateSelection": "選択したテンプレートをクリア", + "choosePromptTemplate": "プロンプトテンプレートを選択", + "myTemplates": "自分のテンプレート", + "flatten": "選択中のテンプレートをプロンプトに展開", + "uploadImage": "画像をアップロード", + "defaultTemplates": "デフォルトテンプレート", + "createPromptTemplate": "プロンプトテンプレートを作成", + "promptTemplateCleared": "プロンプトテンプレートをクリアしました", + "searchByName": "名前で検索", + "toggleViewMode": "表示モードを切り替え", + "negativePromptColumn": "'negative_prompt'", + "preview": "プレビュー", + "nameColumn": "'name'", + "type": "タイプ", + "private": "プライベート", + "name": "名称", + "active": "アクティブ", + "copyTemplate": "テンプレートをコピー", + "deleteImage": "画像を削除", + "deleteTemplate": "テンプレートを削除", + "deleteTemplate2": "このテンプレートを削除してもよろしいですか? 元に戻すことはできません。", + "exportPromptTemplates": "プロンプトテンプレートをエクスポートする(CSV)", + "editTemplate": "テンプレートを編集", + "exportDownloaded": "エクスポートをダウンロードしました", + "exportFailed": "生成とCSVのダウンロードができません", + "importTemplates": "プロンプトテンプレートのインポート(CSV/JSON)", + "acceptedColumnsKeys": "受け入れられる列/キー:", + "positivePromptColumn": "'prompt'または'positive_prompt'", + "insertPlaceholder": "プレースホルダーを挿入", + "negativePrompt": "ネガティブプロンプト", + "noTemplates": "テンプレートがありません", + "noMatchingTemplates": "一致するテンプレートがありません", + "promptTemplatesDesc1": "プロンプトテンプレートは、プロンプトボックスに書き込むプロンプトにテキストを追加します。", + "promptTemplatesDesc2": "テンプレート内でプロンプトを含める場所を指定するには
{{placeholder}}
のプレースホルダーの文字列を使用します。", + "promptTemplatesDesc3": "プレースホルダーを省略すると、テンプレートはプロンプトの末尾に追加されます。", + "positivePrompt": "ポジティブプロンプト", + "shared": "共有", + "sharedTemplates": "テンプレートを共有", + "templateDeleted": "プロンプトテンプレートを削除しました", + "unableToDeleteTemplate": "プロンプトテンプレートを削除できません", + "updatePromptTemplate": "プロンプトテンプレートをアップデート", + "useForTemplate": "プロンプトテンプレートに使用する", + "viewList": "テンプレートリストを表示", + "viewModeTooltip": "現在選択されているテンプレートでは、プロンプトはこのようになります。プロンプトを編集するには、テキストボックス内の任意の場所をクリックしてください。", + "togglePromptPreviews": "プロンプトプレビューを切り替える" + }, + "upscaling": { + "upscaleModel": "アップスケールモデル", + "postProcessingModel": "ポストプロセスモデル", + "upscale": "アップスケール", + "scale": "スケール", + "creativity": "創造性", + "exceedsMaxSize": "アップスケール設定が最大サイズ制限を超えています", + "exceedsMaxSizeDetails": "アップスケールの上限は{{max Upscale Dimension}} x {{max Upscale Dimension}}ピクセルです。画像を小さくするか、スケールの選択範囲を小さくしてください。", + "structure": "構造", + "postProcessingMissingModelWarning": "後処理 (img2img) モデルをインストールするには、モデル マネージャー にアクセスしてください。", + "missingModelsWarning": "必要なモデルをインストールするには、モデル マネージャー にアクセスしてください。", + "mainModelDesc": "メインモデル(SD1.5またはSDXLアーキテクチャ)", + "tileControlNetModelDesc": "選択したメインモデルアーキテクチャのタイルコントロールネットモデル", + "upscaleModelDesc": "アップスケール(img2img)モデル", + "missingUpscaleInitialImage": "アップスケール用の初期画像がありません", + "missingUpscaleModel": "アップスケールモデルがありません", + "missingTileControlNetModel": "有効なタイル コントロールネットモデルがインストールされていません", + "incompatibleBaseModel": "アップスケールにサポートされていないメインモデルアーキテクチャです", + "incompatibleBaseModelDesc": "アップスケールはSD1.5およびSDXLアーキテクチャモデルでのみサポートされています。アップスケールを有効にするには、メインモデルを変更してください。", + "tileControl": "タイルコントロール", + "tileSize": "タイルサイズ", + "tileOverlap": "タイルオーバーラップ" + }, + "sdxl": { + "denoisingStrength": "ノイズ強度", + "scheduler": "スケジューラ", + "loading": "ロード中...", + "steps": "ステップ", + "refiner": "リファイナー", + "noModelsAvailable": "利用できるモデルがありません", + "cfgScale": "CFGスケール", + "posAestheticScore": "ポジティブ美的スコア", + "refinerSteps": "リファイナーステップ", + "refinerStart": "リファイナースタート", + "refinermodel": "リファイナーモデル", + "negAestheticScore": "ネガティブ美的スコア", + "concatPromptStyle": "プロンプトとスタイルのリンク", + "freePromptStyle": "手動スタイルプロンプト", + "negStylePrompt": "ネガティブスタイルのプロンプト", + "posStylePrompt": "ポジティブスタイルのプロンプト" + }, + "modelCache": { + "clear": "モデルキャッシュを消去", + "clearSucceeded": "モデルキャッシュを消去しました", + "clearFailed": "モデルキャッシュの消去中に問題が発生" + }, + "workflows": { + "workflows": "ワークフロー", + "ascending": "昇順", + "name": "名前", + "descending": "降順", + "searchPlaceholder": "名前、説明、タグで検索", + "updated": "アップデート", + "published": "公表", + "builder": { + "label": "ラベル", + "containerPlaceholder": "空のコンテナ", + "showDescription": "説明を表示", + "emptyRootPlaceholderEditMode": "開始するには、フォーム要素またはノード フィールドをここにドラッグします。", + "divider": "区切り線", + "deleteAllElements": "すべてのフォーム要素を削除", + "heading": "見出し", + "nodeField": "ノードフィールド", + "zoomToNode": "ノードにズーム", + "dropdown": "ドロップダウン", + "resetOptions": "オプションをリセット", + "both": "両方", + "builder": "フォームビルダー", + "text": "テキスト", + "row": "横", + "multiLine": "テキスト(複数行)", + "resetAllNodeFields": "すべてのノードフィールドをリセット", + "slider": "スライダー", + "layout": "レイアウト", + "addToForm": "フォームに追加", + "headingPlaceholder": "空の見出し", + "nodeFieldTooltip": "ノード フィールドを追加するには、ワークフロー エディターのフィールドにある小さなプラス記号ボタンをクリックするか、フィールド名をフォームにドラッグします。", + "component": "コンポーネント", + "textPlaceholder": "空のテキスト", + "addOption": "オプションを追加", + "singleLine": "テキスト", + "numberInput": "数値入力", + "column": "縦", + "container": "コンテナ", + "containerRowLayout": "コンテナ(横レイアウト)", + "containerColumnLayout": "コンテナ(縦レイアウト)", + "maximum": "最大", + "published": "公開済み", + "publishedWorkflowOutputs": "アウトプット", + "minimum": "最小", + "publish": "公開", + "unpublish": "非公開", + "publishedWorkflowInputs": "インプット", + "workflowLocked": "ワークフローがロックされました", + "workflowLockedPublished": "公開済みのワークフローは編集用にロックされています。\nワークフローを非公開にして編集したり、コピーを作成したりできます。", + "workflowLockedDuringPublishing": "公開の構成中にワークフローがロックされます。", + "selectOutputNode": "出力ノードを選択", + "changeOutputNode": "出力ノードの変更", + "unpublishableInputs": "これらの公開できない入力は省略されます", + "noPublishableInputs": "公開可能な入力はありません", + "noOutputNodeSelected": "出力ノードが選択されていません", + "cannotPublish": "ワークフローを公開できません", + "publishWarnings": "警告", + "errorWorkflowHasUnsavedChanges": "ワークフローに保存されていない変更があります", + "errorWorkflowHasUnpublishableNodes": "ワークフローにはバッチ、ジェネレータ、またはメタデータ抽出ノードがあります", + "errorWorkflowHasInvalidGraph": "ワークフロー グラフが無効です (詳細については [呼び出し] ボタンにマウスを移動してください)", + "errorWorkflowHasNoOutputNode": "出力ノードが選択されていません", + "warningWorkflowHasNoPublishableInputFields": "パブリッシュ可能な入力フィールドが選択されていません - パブリッシュされたワークフローはデフォルト値のみで実行されます", + "warningWorkflowHasUnpublishableInputFields": "ワークフローには公開できない入力がいくつかあります。これらは公開されたワークフローから省略されます", + "publishFailed": "公開失敗", + "publishFailedDesc": "ワークフローの公開中に問題が発生しました。もう一度お試しください。", + "publishSuccess": "ワークフローを公開しています", + "publishSuccessDesc": "プロジェクト ダッシュボード をチェックして進捗状況を確認してください。", + "publishInProgress": "公開中", + "publishedWorkflowIsLocked": "公開されたワークフローはロックされています", + "publishingValidationRun": "パブリッシュ用検証を実行", + "publishingValidationRunInProgress": "パブリッシュ用検証を実行中です。", + "publishedWorkflowsLocked": "パブリッシュ済みのワークフローはロックされ編集や実行はできません。このワークフローを編集または実行するには、ワークフローのパブリッシュを取りやめるか、コピーを保存してください。", + "selectingOutputNode": "出力ノードの選択", + "selectingOutputNodeDesc": "ノードをクリックして、ワークフローの出力ノードとして選択します。", + "removeFromForm": "フォームから削除", + "showShuffle": "シャッフルを表示", + "shuffle": "シャッフル", + "emptyRootPlaceholderViewMode": "このワークフローのフォームの作成を開始するには、[編集] をクリックします。", + "workflowBuilderAlphaWarning": "ワークフロービルダーは現在アルファ版です。安定版リリースまでに互換性に影響する変更が発生する可能性があります。" + }, + "chooseWorkflowFromLibrary": "ライブラリからワークフローを選択", + "unnamedWorkflow": "名前のないワークフロー", + "download": "ダウンロード", + "savingWorkflow": "ワークフローを保存しています...", + "problemSavingWorkflow": "ワークフローの保存に関する問題", + "convertGraph": "グラフを変換", + "downloadWorkflow": "ファイルに保存", + "saveWorkflow": "ワークフローを保存", + "yourWorkflows": "あなたのワークフロー", + "edit": "編集", + "workflowLibrary": "ワークフローライブラリ", + "workflowSaved": "ワークフローが保存されました", + "workflowCleared": "ワークフローが作成されました", + "autoLayout": "オートレイアウト", + "view": "ビュー", + "saveChanges": "変更を保存", + "recommended": "あなたへのおすすめ", + "newWorkflowCreated": "新しいワークフローが作成されました", + "noWorkflows": "ワークフローがありません", + "copyShareLink": "共有リンクをコピー", + "copyShareLinkForWorkflow": "ワークフローの共有リンクをコピー", + "workflowThumbnail": "ワークフローサムネイル", + "loadWorkflow": "$t(common.load) ワークフロー", + "shared": "共有", + "emptyStringPlaceholder": "<空の文字列>", + "browseWorkflows": "ワークフローを閲覧する", + "saveWorkflowAs": "ワークフローとして保存", + "private": "プライベート", + "deselectAll": "すべて選択解除", + "delete": "削除", + "loadMore": "もっと読み込む", + "saveWorkflowToProject": "ワークフローをプロジェクトに保存", + "created": "作成順", + "workflowEditorMenu": "ワークフローエディターメニュー", + "recentlyOpened": "最近開いた", + "opened": "オープン", + "deleteWorkflow": "ワークフローを削除", + "deleteWorkflow2": "このワークフローを削除してもよろしいですか? 元に戻すことはできません。", + "loadFromGraph": "グラフからワークフローをロード", + "workflowName": "ワークフロー名", + "loading": "ワークフローをロードしています", + "uploadWorkflow": "ファイルから読み込み", + "defaultWorkflows": "デフォルトワークフロー", + "userWorkflows": "ユーザーワークフロー", + "projectWorkflows": "プロジェクトワークフロー", + "allLoaded": "すべてのワークフローが読み込まれました", + "filterByTags": "タグでフィルター", + "noRecentWorkflows": "最近のワークフローはありません", + "openWorkflow": "ワークフローを開く", + "problemLoading": "ワークフローの読み込み中に問題が発生しました", + "noDescription": "説明なし", + "searchWorkflows": "ワークフローを検索", + "clearWorkflowSearchFilter": "ワークフロー検索フィルターをクリア", + "openLibrary": "ライブラリを開く" + }, + "system": { + "logNamespaces": { + "system": "システム", + "gallery": "ギャラリー", + "workflows": "ワークフロー", + "models": "モデル", + "canvas": "キャンバス", + "metadata": "メタデータ", + "queue": "キュー", + "logNamespaces": "ログのネームスペース", + "dnd": "ドラッグ&ドロップ", + "config": "構成", + "generation": "生成", + "events": "イベント" + }, + "logLevel": { + "debug": "Debug", + "info": "Info", + "error": "Error", + "fatal": "Fatal", + "warn": "Warn", + "logLevel": "ログレベル", + "trace": "追跡" + }, + "enableLogging": "ログを有効にする" + }, + "dynamicPrompts": { + "promptsPreview": "プロンプトプレビュー", + "seedBehaviour": { + "label": "シードの挙動", + "perPromptLabel": "画像毎のシード", + "perIterationLabel": "イテレーション毎のシード", + "perPromptDesc": "それぞれの画像で別のシードを使う", + "perIterationDesc": "それぞれのイテレーションに別のシードを使う" + }, + "showDynamicPrompts": "ダイナミックプロンプトを表示", + "dynamicPrompts": "ダイナミックプロンプト", + "loading": "ダイナミックプロンプトを生成中...", + "maxPrompts": "最大プロンプト", + "promptsToGenerate": "生成するプロンプト" + }, + "newUserExperience": { + "toGetStartedLocal": "始めるには、Invoke の実行に必要なモデルをダウンロードまたはインポートしてください。次に、ボックスにプロンプトを入力し、Invoke をクリックして最初の画像を生成します。プロンプトテンプレートを選択すると、結果が向上します。画像は Gallery に直接保存するか、Canvas で編集するかを選択できます。", + "toGetStarted": "ボックスにプロンプトを入力し、Invoke をクリックして最初の画像を生成します。プロンプトテンプレートを選択すると、結果が向上します。画像は Gallery に直接保存するか、Canvas で編集するかを選択できます。", + "toGetStartedWorkflow": "左側のフィールドに入力し、Invoke をクリックして画像を生成します。他のワークフローも試してみたい場合は、ワークフロータイトルの横にあるフォルダアイコン をクリックすると、試せる他のテンプレートのリストが表示されます。", + "gettingStartedSeries": "さらに詳しいガイダンスが必要ですか? Invoke Studio の可能性を最大限に引き出すためのヒントについては、入門シリーズをご覧ください。", + "lowVRAMMode": "最高のパフォーマンスを得るには、低 VRAM ガイドに従ってください。", + "noModelsInstalled": "モデルがインストールされていないようです。スターターモデルバンドルをダウンロードするか、モデルをインポートしてください。" + }, + "whatsNew": { + "whatsNewInInvoke": "Invokeの新機能", + "items": [ + "LLMプロンプトツール:ローカルLLMでプロンプトを拡張したり、画像からプロンプトを生成できます。使用するにはLLM(例:Qwen2.5-1.5B-Instruct)をインストールしてください。", + "ラスター レイヤーの調整: レイヤーの明度、コントラスト、彩度、カーブなどを簡単に調整できます。" + ], + "readReleaseNotes": "リリースノートを読む", + "watchRecentReleaseVideos": "最近のリリースビデオを見る", + "watchUiUpdatesOverview": "Watch UI アップデートの概要" + }, + "supportVideos": { + "supportVideos": "サポートビデオ", + "gettingStarted": "はじめる", + "watch": "ウォッチ", + "studioSessionsDesc": " に参加してライブセッションに参加したり、質問したりしてください。セッションは翌週にプレイリストにアップロードされます。", + "videos": { + "gettingStarted": { + "title": "Invokeを使い始める", + "description": "最初のイメージの作成から高度なテクニックまで、Invoke を使い始めるために知っておく必要のあるすべての内容を網羅した完全なビデオ シリーズです。" + }, + "studioSessions": { + "title": "スタジオセッション", + "description": "高度な Invoke 機能、クリエイティブなワークフロー、コミュニティのディスカッションについて詳しく説明するセッションです。" + } + } + }, + "lora": { + "weight": "重み", + "removeLoRA": "LoRAを解除" + }, + "auth": { + "login": { + "title": "Invokeにサインイン", + "email": "Eメール", + "emailPlaceholder": "Eメール", + "password": "パスワード", + "passwordPlaceholder": "パスワード", + "rememberMe": "7日間は記憶", + "signIn": "サインイン", + "signingIn": "サインイン中...", + "loginFailed": "ログインに失敗しました。正しい内容かを確認してください。", + "sessionExpired": "認証情報が期限切れです。再度ログインして再開してください。" + }, + "setup": { + "title": "Invokeへようこそ", + "subtitle": "管理者アカウントをセットアップします", + "email": "Eメール", + "emailPlaceholder": "hoge@example.com", + "emailHelper": "これはサインインに使うユーザー名になります", + "displayName": "表示名", + "displayNamePlaceholder": "管理者", + "displayNameHelper": "アプリケーションの中で表示される名前です", + "password": "パスワード", + "passwordPlaceholder": "パスワード", + "passwordHelper": "大文字、小文字、数字を組み合わせた8文字以上", + "passwordTooShort": "パスワードは8文字以上である必要があります", + "passwordMissingRequirements": "パスワードは小文字、大文字、数字を含まなければなりません", + "confirmPassword": "パスワードの確認", + "confirmPasswordPlaceholder": "パスワードの確認", + "passwordsDoNotMatch": "パスワードが一致しません", + "createAccount": "管理者アカウントを作る", + "creatingAccount": "設定中...", + "setupFailed": "セットアップに失敗しました。もう一度試してください。", + "passwordHelperRelaxed": "パスワードを入力してください(強度が表示されます)" + }, + "userMenu": "ユーザーメニュー", + "admin": "管理", + "logout": "ログアウト", + "adminOnlyFeature": "この機能は管理者のみ使用できます。", + "profile": { + "menuItem": "プロフィール", + "title": "プロフィール", + "email": "Eメール", + "emailReadOnly": "Eメールアドレスは変更できません", + "displayName": "表示名", + "displayNamePlaceholder": "あなたの名前", + "changePassword": "パスワードの変更", + "currentPassword": "現在のパスワード", + "currentPasswordPlaceholder": "現在のパスワード", + "newPassword": "新しいパスワード", + "newPasswordPlaceholder": "新しいパスワード", + "confirmPassword": "新しいパスワードの確認", + "confirmPasswordPlaceholder": "新しいパスワードの確認", + "passwordsDoNotMatch": "パスワードが一致しません", + "saveSuccess": "プロフィールのアップデートに成功しました", + "saveFailed": "プロフィールの保存に失敗しました。もう一度試してください。" + }, + "userManagement": { + "menuItem": "ユーザー管理", + "title": "ユーザー管理", + "email": "Eメール", + "emailPlaceholder": "hoge@example.com", + "displayName": "表示名", + "displayNamePlaceholder": "表示名", + "password": "パスワード", + "passwordPlaceholder": "パスワード", + "newPassword": "新しいパスワード", + "newPasswordPlaceholder": "現在のパスワードを維持するには空白にしておいてください", + "role": "ロール", + "status": "ステータス", + "actions": "アクション", + "isAdmin": "管理者", + "user": "ユーザー", + "you": "あなた", + "createUser": "ユーザーの作成", + "editUser": "ユーザーの編集", + "deleteUser": "ユーザーの削除", + "deleteConfirm": "本当に \"{{name}}\" を削除しますか?このアクションは取り消せません。", + "generatePassword": "強力なパスワードを生成", + "showPassword": "パスワードの表示", + "hidePassword": "パスワードを隠す", + "activate": "有効化", + "deactivate": "非有効化", + "saveFailed": "ユーザーの保存に失敗しました。もう一度実行してください。", + "deleteFailed": "ユーザーの削除に失敗しました。もう一度実行してください。", + "loadFailed": "ユーザーのロードに失敗しました。", + "back": "戻る", + "cannotDeleteSelf": "あなた自身のアカウントを削除することはできません", + "cannotDeactivateSelf": "あなた自身のアカウントを非有効化することはできません" + }, + "passwordStrength": { + "weak": "弱いパスワード", + "moderate": "適切なパスワード", + "strong": "強力なパスワード" + } + }, + "customNodes": { + "title": "カスタムノード", + "installTitle": "ノードパックのインストール", + "gitUrl": "GitリポジトリURL", + "gitUrlLabel": "リポジトリURL", + "install": "インストール", + "installing": "インストール中", + "installSuccess": "ノードパックがインストールされました", + "installFailed": "インストールが失敗しました", + "installError": "インストール中に予期しないエラーが発生しました。", + "securityWarning": "ノードパックは信頼できる開発元からのみインストールししてください。カスタムノードはシステム上でコードを実行します。 悪意のあるノードはシステムに損害を及ぼしたり、データを破壊する可能性があります。", + "installDescription": "リポジトリをノードディレクトリにクローンします。 ワークフローファイル(.json)はライブラリにインポートされます。 Python の依存関係 (requirements.txt または pyproject.toml ) は自動的にインストールされません。ノードパックのドキュメントに従って手動でインストールしてください。", + "dependenciesRequiredTitle": "依存関係の手動インストールが必要です", + "dependenciesRequiredDescription": "'{{name}}' には {{file}} が含まれています。 ノードパックのドキュメントに従って、ノードを使用する前にPythonの依存関係をインストールしてください。", + "uninstall": "アンインストール", + "reload": "再読み込み", + "reloading": "再読み込み中", + "noNodePacks": "カスタムノードパックがインストールされていません。", + "scanFolder": "フォルダーをスキャン", + "scanFolderDescription": "nodes ディレクトリに置いた Node パックは起動時に自動検出されます。 再読み込みボタン押すと、再起動せずに新しく追加されたパックを検出できます。", + "nodesDirectory": "ノードディレクトリ", + "installQueue": "インストール履歴", + "queueEmpty": "最近インストールされた履歴はありません。", + "name": "名前", + "message": "メッセージ", + "nodeCount_other": "{{count}} ノード", + "uninstalled": "アンインストール済み" + } +} diff --git a/invokeai/frontend/web/public/locales/ko.json b/invokeai/frontend/web/public/locales/ko.json new file mode 100644 index 00000000000..6ead01bd1ba --- /dev/null +++ b/invokeai/frontend/web/public/locales/ko.json @@ -0,0 +1,326 @@ +{ + "common": { + "languagePickerLabel": "언어 설정", + "reportBugLabel": "버그 리포트", + "githubLabel": "Github", + "settingsLabel": "설정", + "nodes": "Workflow Editor", + "upload": "업로드", + "load": "불러오기", + "back": "뒤로 가기", + "statusDisconnected": "연결 끊김", + "hotkeysLabel": "단축키 설정", + "img2img": "이미지->이미지", + "discordLabel": "Discord", + "t2iAdapter": "T2I 어댑터", + "communityLabel": "커뮤니티", + "txt2img": "텍스트->이미지", + "dontAskMeAgain": "다시 묻지 마세요", + "checkpoint": "체크포인트", + "format": "형식", + "unknown": "알려지지 않음", + "areYouSure": "확실하나요?", + "folder": "폴더", + "inpaint": "inpaint", + "updated": "업데이트 됨", + "on": "켜기", + "save": "저장", + "created": "생성됨", + "error": "에러", + "ipAdapter": "IP 어댑터", + "installed": "설치됨", + "accept": "수락", + "ai": "인공지능", + "auto": "자동", + "file": "파일", + "openInNewTab": "새 탭에서 열기", + "delete": "삭제", + "template": "템플릿", + "cancel": "취소", + "controlNet": "컨트롤넷", + "outputs": "결과물", + "unknownError": "알려지지 않은 에러", + "linear": "선형", + "direction": "방향", + "data": "데이터", + "somethingWentWrong": "뭔가 잘못됐어요", + "modelManager": "Model Manager", + "safetensors": "Safetensors", + "outpaint": "outpaint", + "orderBy": "정렬 기준", + "copyError": "$t(gallery.copy) 에러", + "learnMore": "더 알아보기", + "saveAs": "다른 이름으로 저장", + "loading": "불러오는 중", + "random": "랜덤", + "batch": "Batch 매니저", + "postprocessing": "후처리", + "advanced": "고급", + "input": "입력", + "details": "세부사항" + }, + "gallery": { + "galleryImageSize": "이미지 크기", + "gallerySettings": "갤러리 설정", + "deleteSelection": "선택 항목 삭제", + "featuresWillReset": "이 이미지를 삭제하면 해당 기능이 즉시 재설정됩니다.", + "autoSwitchNewImages": "새로운 이미지로 자동 전환", + "loading": "불러오는 중", + "image": "이미지", + "drop": "드랍", + "downloadSelection": "선택 항목 다운로드", + "deleteImage_other": "이미지 삭제", + "currentlyInUse": "이 이미지는 현재 다음 기능에서 사용되고 있습니다:", + "dropOrUpload": "$t(gallery.drop) 또는 업로드", + "copy": "복사", + "download": "다운로드", + "deleteImagePermanent": "삭제된 이미지는 복원할 수 없습니다.", + "noImageSelected": "선택된 이미지 없음", + "autoAssignBoardOnClick": "클릭 시 Board로 자동 할당", + "dropToUpload": "업로드를 위해 $t(gallery.drop)" + }, + "accessibility": { + "previousImage": "이전 이미지", + "nextImage": "다음 이미지", + "mode": "모드", + "menu": "메뉴", + "uploadImage": "이미지 업로드", + "reset": "리셋" + }, + "modelManager": { + "availableModels": "사용 가능한 모델", + "addModel": "모델 추가", + "none": "없음", + "modelConverted": "변환된 모델", + "width": "너비", + "convert": "변환", + "vae": "VAE", + "deleteModel": "모델 삭제", + "description": "Description", + "search": "검색", + "predictionType": "예측 유형(안정 확산 2.x 모델 및 간혹 안정 확산 1.x 모델의 경우)", + "selectModel": "모델 선택", + "repo_id": "Repo ID", + "convertToDiffusersHelpText6": "이 모델을 변환하시겠습니까?", + "config": "구성", + "selected": "선택된", + "advanced": "고급", + "load": "불러오기", + "height": "높이", + "modelDeleted": "삭제된 모델", + "convertToDiffusersHelpText2": "이 프로세스는 모델 관리자 항목을 동일한 모델의 Diffusers 버전으로 대체합니다.", + "modelUpdateFailed": "모델 업데이트 실패", + "modelUpdated": "업데이트된 모델", + "settings": "설정", + "convertToDiffusersHelpText5": "디스크 공간이 충분한지 확인해 주세요. 모델은 일반적으로 2GB에서 7GB 사이로 다양합니다.", + "deleteConfig": "구성 삭제", + "modelConversionFailed": "모델 변환 실패", + "deleteMsg1": "InvokeAI에서 이 모델을 삭제하시겠습니까?", + "syncModels": "동기화 모델", + "modelType": "모델 유형", + "convertingModelBegin": "모델 변환 중입니다. 잠시만 기다려 주십시오.", + "name": "이름", + "convertToDiffusersHelpText1": "이 모델은 🧨 Diffusers 형식으로 변환됩니다.", + "vaePrecision": "VAE 정밀도", + "deleteMsg2": "모델이 InvokeAI root 폴더에 있으면 디스크에서 모델이 삭제됩니다. 사용자 지정 위치를 사용하는 경우 모델이 디스크에서 삭제되지 않습니다.", + "baseModel": "기본 모델", + "manual": "매뉴얼", + "convertToDiffusersHelpText3": "디스크의 체크포인트 파일이 InvokeAI root 폴더에 있으면 삭제됩니다. 사용자 지정 위치에 있으면 삭제되지 않습니다.", + "modelManager": "모델 매니저", + "variant": "Variant", + "modelDeleteFailed": "모델을 삭제하지 못했습니다", + "convertToDiffusers": "Diffusers로 변환", + "allModels": "모든 모델", + "alpha": "Alpha", + "noModelSelected": "선택한 모델 없음", + "convertToDiffusersHelpText4": "이것은 한 번의 과정일 뿐입니다. 컴퓨터 사양에 따라 30-60초 정도 소요될 수 있습니다.", + "model": "모델", + "delete": "삭제" + }, + "nodes": { + "missingTemplate": "잘못된 노드: {{type}} 유형의 {{node}} 템플릿 누락(설치되지 않으셨나요?)", + "noNodeSelected": "선택한 노드 없음", + "addNode": "노드 추가", + "enum": "Enum", + "loadWorkflow": "Workflow 불러오기", + "noOutputRecorded": "기록된 출력 없음", + "colorCodeEdgesHelp": "연결된 필드에 따른 색상 코드 선", + "float": "실수", + "targetNodeFieldDoesNotExist": "잘못된 모서리: 대상/입력 필드 {{node}}. {{field}}이(가) 없습니다", + "animatedEdges": "애니메이션 모서리", + "integer": "정수", + "nodeTemplate": "노드 템플릿", + "nodeOpacity": "노드 불투명도", + "sourceNodeDoesNotExist": "잘못된 모서리: 소스/출력 노드 {{node}}이(가) 없습니다", + "nodeSearch": "노드 검색", + "inputMayOnlyHaveOneConnection": "입력에 하나의 연결만 있을 수 있습니다", + "notes": "메모", + "nodeOutputs": "노드 결과물", + "currentImageDescription": "Node Editor에 현재 이미지를 표시합니다", + "downloadWorkflow": "Workflow JSON 다운로드", + "ipAdapter": "IP-Adapter", + "noConnectionInProgress": "진행중인 연결이 없습니다", + "fieldTypesMustMatch": "필드 유형은 일치해야 합니다", + "edge": "Edge", + "sourceNodeFieldDoesNotExist": "잘못된 모서리: 소스/출력 필드 {{node}}. {{field}}이(가) 없습니다", + "animatedEdgesHelp": "선택한 노드에 연결된 선택한 가장자리 및 가장자리를 애니메이션화합니다", + "cannotDuplicateConnection": "중복 연결을 만들 수 없습니다", + "noWorkflow": "Workflow 없음", + "fullyContainNodesHelp": "선택하려면 노드가 선택 상자 안에 완전히 있어야 합니다", + "nodePack": "Node pack", + "nodeType": "노드 유형", + "fullyContainNodes": "선택할 노드 전체 포함", + "executionStateInProgress": "진행중", + "executionStateError": "에러", + "boolean": "Booleans", + "hideMinimapnodes": "미니맵 숨기기", + "executionStateCompleted": "완료된", + "node": "노드", + "currentImage": "현재 이미지", + "collection": "컬렉션", + "cannotConnectInputToInput": "입력을 입력에 연결할 수 없습니다", + "collectionFieldType": "{{name}} 컬렉션", + "cannotConnectOutputToOutput": "출력을 출력에 연결할 수 없습니다", + "connectionWouldCreateCycle": "연결하면 주기가 생성됩니다", + "cannotConnectToSelf": "자체에 연결할 수 없습니다", + "notesDescription": "Workflow에 대한 메모 추가", + "colorCodeEdges": "색상-코드 선", + "targetNodeDoesNotExist": "잘못된 모서리: 대상/입력 노드 {{node}}이(가) 없습니다", + "addNodeToolTip": "노드 추가(Shift+A, Space)", + "collectionOrScalarFieldType": "{{name}} 컬렉션|Scalar", + "nodeVersion": "노드 버전", + "loadingNodes": "노드 로딩중...", + "deletedInvalidEdge": "잘못된 모서리 {{source}} -> {{target}} 삭제" + }, + "queue": { + "status": "상태", + "pruneSucceeded": "Queue로부터 {{item_count}} 완성된 항목 잘라내기", + "cancelTooltip": "현재 항목 취소", + "queueEmpty": "비어있는 Queue", + "pauseSucceeded": "중지된 프로세서", + "in_progress": "진행 중", + "queueFront": "Front of Queue에 추가", + "notReady": "Queue를 생성할 수 없음", + "batchFailedToQueue": "Queue Batch에 실패", + "completed": "완성된", + "queueBack": "Queue에 추가", + "cancelFailed": "항목 취소 중 발생한 문제", + "batchQueued": "Batch Queued", + "pauseFailed": "프로세서 중지 중 발생한 문제", + "clearFailed": "Queue 제거 중 발생한 문제", + "front": "front", + "clearSucceeded": "제거된 Queue", + "pause": "중지", + "pruneTooltip": "{{item_count}} 완성된 항목 잘라내기", + "cancelSucceeded": "취소된 항목", + "batchQueuedDesc_other": "queue의 {{direction}}에 추가된 {{count}}세션", + "queue": "Queue", + "batch": "Batch", + "clearQueueAlertDialog": "Queue를 지우면 처리 항목이 즉시 취소되고 Queue가 완전히 지워집니다.", + "resumeFailed": "프로세서 재개 중 발생한 문제", + "clear": "제거하다", + "prune": "잘라내다", + "total": "총 개수", + "canceled": "취소된", + "pruneFailed": "Queue 잘라내는 중 발생한 문제", + "cancelBatchSucceeded": "취소된 Batch", + "clearTooltip": "모든 항목을 취소하고 제거", + "current": "최근", + "pauseTooltip": "프로세서 중지", + "failed": "실패한", + "cancelItem": "항목 취소", + "next": "다음", + "cancelBatch": "Batch 취소", + "back": "back", + "cancel": "취소", + "session": "세션", + "time": "시간", + "resumeSucceeded": "재개된 프로세서", + "enqueueing": "Queueing Batch", + "resumeTooltip": "프로세서 재개", + "resume": "재개", + "cancelBatchFailed": "Batch 취소 중 발생한 문제", + "clearQueueAlertDialog2": "Queue를 지우시겠습니까?", + "item": "항목", + "graphFailedToQueue": "queue graph에 실패" + }, + "metadata": { + "positivePrompt": "긍정적 프롬프트", + "negativePrompt": "부정적인 프롬프트", + "generationMode": "Generation Mode", + "Threshold": "Noise Threshold", + "metadata": "Metadata", + "seed": "시드", + "imageDetails": "이미지 세부 정보", + "model": "모델", + "noImageDetails": "이미지 세부 정보를 찾을 수 없습니다", + "cfgScale": "CFG scale", + "recallParameters": "매개변수 호출", + "height": "Height", + "noMetaData": "metadata를 찾을 수 없습니다", + "cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)", + "width": "너비", + "vae": "VAE", + "createdBy": "~에 의해 생성된", + "workflow": "작업의 흐름", + "steps": "단계", + "scheduler": "스케줄러", + "noRecallParameters": "호출할 매개 변수가 없습니다" + }, + "invocationCache": { + "useCache": "캐시 사용", + "disable": "이용 불가능한", + "misses": "캐시 미스", + "enableFailed": "Invocation 캐시를 사용하도록 설정하는 중 발생한 문제", + "invocationCache": "Invocation 캐시", + "clearSucceeded": "제거된 Invocation 캐시", + "enableSucceeded": "이용 가능한 Invocation 캐시", + "clearFailed": "Invocation 캐시 제거 중 발생한 문제", + "hits": "캐시 적중", + "disableSucceeded": "이용 불가능한 Invocation 캐시", + "disableFailed": "Invocation 캐시를 이용하지 못하게 설정 중 발생한 문제", + "enable": "이용 가능한", + "clear": "제거", + "maxCacheSize": "최대 캐시 크기", + "cacheSize": "캐시 크기" + }, + "hrf": { + "metadata": { + "strength": "고해상도 고정 강도", + "enabled": "고해상도 고정 사용", + "method": "고해상도 고정 방법" + }, + "hrf": "고해상도 고정" + }, + "models": { + "noMatchingModels": "일치하는 모델 없음", + "loading": "로딩중", + "noModelsAvailable": "사용 가능한 모델이 없음", + "addLora": "LoRA 추가", + "selectModel": "모델 선택", + "noRefinerModelsInstalled": "SDXL Refiner 모델이 설치되지 않음" + }, + "boards": { + "autoAddBoard": "자동 추가 Board", + "topMessage": "이 보드에는 다음 기능에 사용되는 이미지가 포함되어 있습니다:", + "move": "이동", + "menuItemAutoAdd": "해당 Board에 자동 추가", + "myBoard": "나의 Board", + "searchBoard": "Board 찾는 중...", + "deleteBoardOnly": "Board만 삭제", + "noMatching": "일치하는 Board들이 없음", + "movingImagesToBoard_other": "{{count}}이미지를 Board로 이동시키기", + "selectBoard": "Board 선택", + "cancel": "취소", + "addBoard": "Board 추가", + "bottomMessage": "이 보드와 이미지를 삭제하면 현재 사용 중인 모든 기능이 재설정됩니다.", + "uncategorized": "미분류", + "downloadBoard": "Board 다운로드", + "changeBoard": "Board 바꾸기", + "loading": "불러오는 중...", + "clearSearch": "검색 지우기", + "deleteBoard": "Board 삭제", + "deleteBoardAndImages": "Board와 이미지 삭제", + "deletedBoardsCannotbeRestored": "삭제된 Board는 복원할 수 없습니다" + } +} diff --git a/invokeai/frontend/web/public/locales/mn.json b/invokeai/frontend/web/public/locales/mn.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/invokeai/frontend/web/public/locales/mn.json @@ -0,0 +1 @@ +{} diff --git a/invokeai/frontend/web/public/locales/nl.json b/invokeai/frontend/web/public/locales/nl.json new file mode 100644 index 00000000000..2da6709348e --- /dev/null +++ b/invokeai/frontend/web/public/locales/nl.json @@ -0,0 +1,806 @@ +{ + "common": { + "hotkeysLabel": "Sneltoetsen", + "languagePickerLabel": "Taal", + "reportBugLabel": "Meld bug", + "settingsLabel": "Instellingen", + "img2img": "Afbeelding naar afbeelding", + "nodes": "Werkstromen", + "upload": "Upload", + "load": "Laad", + "statusDisconnected": "Niet verbonden", + "githubLabel": "Github", + "discordLabel": "Discord", + "back": "Terug", + "cancel": "Annuleer", + "accept": "Akkoord", + "loading": "Bezig met laden", + "txt2img": "Tekst naar afbeelding", + "postprocessing": "Naverwerking", + "dontAskMeAgain": "Vraag niet opnieuw", + "random": "Willekeurig", + "openInNewTab": "Open in nieuw tabblad", + "areYouSure": "Weet je het zeker?", + "linear": "Lineair", + "batch": "Seriebeheer", + "modelManager": "Modelbeheer", + "communityLabel": "Gemeenschap", + "t2iAdapter": "T2I-adapter", + "on": "Aan", + "ipAdapter": "IP-adapter", + "auto": "Autom.", + "controlNet": "ControlNet", + "learnMore": "Meer informatie", + "advanced": "Uitgebreid", + "file": "Bestand", + "installed": "Geïnstalleerd", + "simple": "Eenvoudig", + "somethingWentWrong": "Er ging iets mis", + "add": "Voeg toe", + "checkpoint": "Checkpoint", + "details": "Details", + "outputs": "Uitvoeren", + "save": "Bewaar", + "blue": "Blauw", + "alpha": "Alfa", + "red": "Rood", + "editor": "Editor", + "folder": "Map", + "format": "structuur", + "template": "Sjabloon", + "input": "Invoer", + "safetensors": "Safetensors", + "saveAs": "Bewaar als", + "created": "Gemaakt", + "green": "Groen", + "tab": "Tab", + "positivePrompt": "Positieve prompt", + "negativePrompt": "Negatieve prompt", + "selected": "Geselecteerd", + "orderBy": "Sorteer op", + "beta": "Bèta", + "copyError": "$t(gallery.copy) Fout", + "toResolve": "Op te lossen", + "aboutDesc": "Gebruik je Invoke voor het werk? Kijk dan naar:", + "aboutHeading": "Creatieve macht voor jou", + "copy": "Kopieer", + "data": "Gegevens", + "or": "of", + "updated": "Bijgewerkt", + "outpaint": "outpainten", + "ai": "ai", + "inpaint": "inpainten", + "unknown": "Onbekend", + "delete": "Verwijder", + "direction": "Richting", + "error": "Fout", + "unknownError": "Onbekende fout" + }, + "gallery": { + "galleryImageSize": "Afbeeldingsgrootte", + "gallerySettings": "Instellingen galerij", + "autoSwitchNewImages": "Wissel autom. naar nieuwe afbeeldingen", + "deleteImage_one": "Verwijder afbeelding", + "deleteImage_other": "", + "deleteImagePermanent": "Verwijderde afbeeldingen kunnen niet worden hersteld.", + "autoAssignBoardOnClick": "Ken automatisch bord toe bij klikken", + "featuresWillReset": "Als je deze afbeelding verwijdert, dan worden deze functies onmiddellijk teruggezet.", + "loading": "Bezig met laden", + "downloadSelection": "Download selectie", + "currentlyInUse": "Deze afbeelding is momenteel in gebruik door de volgende functies:", + "copy": "Kopieer", + "download": "Download" + }, + "modelManager": { + "modelManager": "Modelonderhoud", + "model": "Model", + "modelUpdated": "Model bijgewerkt", + "manual": "Handmatig", + "name": "Naam", + "description": "Beschrijving", + "config": "Configuratie", + "width": "Breedte", + "height": "Hoogte", + "addModel": "Voeg model toe", + "availableModels": "Beschikbare modellen", + "search": "Zoek", + "load": "Laad", + "active": "actief", + "selected": "Gekozen", + "delete": "Verwijder", + "deleteModel": "Verwijder model", + "deleteConfig": "Verwijder configuratie", + "deleteMsg1": "Weet je zeker dat je dit model wilt verwijderen uit InvokeAI?", + "deleteMsg2": "Hiermee ZAL het model van schijf worden verwijderd als het zich bevindt in de beginmap van InvokeAI. Als je het model vanaf een eigen locatie gebruikt, dan ZAL het model NIET van schijf worden verwijderd.", + "convertToDiffusersHelpText3": "Je checkpoint-bestand op de schijf ZAL worden verwijderd als het zich in de beginmap van InvokeAI bevindt. Het ZAL NIET worden verwijderd als het zich in een andere locatie bevindt.", + "convertToDiffusersHelpText6": "Wil je dit model omzetten?", + "allModels": "Alle modellen", + "repo_id": "Repo-id", + "convert": "Omzetten", + "convertToDiffusers": "Omzetten naar Diffusers", + "convertToDiffusersHelpText1": "Dit model wordt omgezet naar de🧨 Diffusers-indeling.", + "convertToDiffusersHelpText2": "Dit proces vervangt het onderdeel in Modelonderhoud met de Diffusers-versie van hetzelfde model.", + "convertToDiffusersHelpText4": "Dit is een eenmalig proces. Dit neemt ongeveer 30 tot 60 sec. in beslag, afhankelijk van de specificaties van je computer.", + "convertToDiffusersHelpText5": "Zorg ervoor dat je genoeg schijfruimte hebt. Modellen nemen gewoonlijk ongeveer 2 tot 7 GB ruimte in beslag.", + "modelConverted": "Model omgezet", + "alpha": "Alfa", + "none": "geen", + "baseModel": "Basismodel", + "vae": "VAE", + "variant": "Variant", + "modelConversionFailed": "Omzetten model mislukt", + "modelUpdateFailed": "Bijwerken model mislukt", + "selectModel": "Kies model", + "settings": "Instellingen", + "modelDeleted": "Model verwijderd", + "syncModels": "Synchroniseer Modellen", + "modelDeleteFailed": "Model kon niet verwijderd worden", + "convertingModelBegin": "Model aan het converteren. Even geduld.", + "predictionType": "Soort voorspelling", + "advanced": "Uitgebreid", + "modelType": "Soort model", + "vaePrecision": "Nauwkeurigheid VAE", + "loraTriggerPhrases": "LoRA-triggerzinnen", + "urlOrLocalPathHelper": "URL's zouden moeten wijzen naar een los bestand. Lokale paden kunnen wijzen naar een los bestand of map voor een individueel Diffusers-model.", + "modelName": "Modelnaam", + "path": "Pad", + "triggerPhrases": "Triggerzinnen", + "typePhraseHere": "Typ zin hier in", + "modelImageDeleteFailed": "Fout bij verwijderen modelafbeelding", + "modelImageUpdated": "Modelafbeelding bijgewerkt", + "modelImageUpdateFailed": "Fout bij bijwerken modelafbeelding", + "noMatchingModels": "Geen overeenkomende modellen", + "scanPlaceholder": "Pad naar een lokale map", + "noModelsInstalled": "Geen modellen geïnstalleerd", + "noModelsInstalledDesc1": "Installeer modellen met de", + "noModelSelected": "Geen model geselecteerd", + "starterModels": "Beginnermodellen", + "textualInversions": "Tekstuele omkeringen", + "upcastAttention": "Upcast-aandacht", + "uploadImage": "Upload afbeelding", + "mainModelTriggerPhrases": "Triggerzinnen hoofdmodel", + "urlOrLocalPath": "URL of lokaal pad", + "scanFolderHelper": "De map zal recursief worden ingelezen voor modellen. Dit kan enige tijd in beslag nemen voor erg grote mappen.", + "simpleModelPlaceholder": "URL of pad naar een lokaal pad of Diffusers-map", + "modelSettings": "Modelinstellingen", + "pathToConfig": "Pad naar configuratie", + "prune": "Snoei", + "pruneTooltip": "Snoei voltooide importeringen uit wachtrij", + "repoVariant": "Repovariant", + "scanFolder": "Lees map in", + "scanResults": "Resultaten inlezen", + "source": "Bron" + }, + "parameters": { + "images": "Afbeeldingen", + "steps": "Stappen", + "cfgScale": "CFG-schaal", + "width": "Breedte", + "height": "Hoogte", + "seed": "Seed", + "shuffle": "Mengseed", + "noiseThreshold": "Drempelwaarde ruis", + "perlinNoise": "Perlinruis", + "type": "Soort", + "strength": "Sterkte", + "upscaling": "Opschalen", + "scale": "Schaal", + "imageFit": "Pas initiële afbeelding in uitvoergrootte", + "scaleBeforeProcessing": "Schalen voor verwerking", + "scaledWidth": "Geschaalde B", + "scaledHeight": "Geschaalde H", + "infillMethod": "Infill-methode", + "tileSize": "Grootte tegel", + "usePrompt": "Hergebruik invoertekst", + "useSeed": "Hergebruik seed", + "useAll": "Hergebruik alles", + "info": "Info", + "symmetry": "Symmetrie", + "cancel": { + "cancel": "Annuleer" + }, + "general": "Algemeen", + "copyImage": "Kopieer afbeelding", + "denoisingStrength": "Sterkte ontruisen", + "scheduler": "Planner", + "seamlessXAxis": "Naadloze tegels in x-as", + "seamlessYAxis": "Naadloze tegels in y-as", + "clipSkip": "Overslaan CLIP", + "negativePromptPlaceholder": "Negatieve prompt", + "controlNetControlMode": "Aansturingsmodus", + "positivePromptPlaceholder": "Positieve prompt", + "maskBlur": "Vervaging van masker", + "invoke": { + "noNodesInGraph": "Geen knooppunten in graaf", + "noModelSelected": "Geen model ingesteld", + "invoke": "Start", + "noPrompts": "Geen prompts gegenereerd", + "missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} invoer ontbreekt", + "systemDisconnected": "Systeem is niet verbonden", + "missingNodeTemplate": "Knooppuntsjabloon ontbreekt", + "missingFieldTemplate": "Veldsjabloon ontbreekt", + "addingImagesTo": "Bezig met toevoegen van afbeeldingen aan" + }, + "patchmatchDownScaleSize": "Verklein", + "useCpuNoise": "Gebruik CPU-ruis", + "imageActions": "Afbeeldingshandeling", + "iterations": "Iteraties", + "coherenceMode": "Modus", + "infillColorValue": "Vulkleur", + "remixImage": "Meng afbeelding opnieuw", + "setToOptimalSize": "Optimaliseer grootte voor het model", + "setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (is mogelijk te klein)", + "aspect": "Beeldverhouding", + "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (is mogelijk te groot)", + "lockAspectRatio": "Zet beeldverhouding vast", + "useSize": "Gebruik grootte", + "swapDimensions": "Wissel afmetingen om", + "coherenceEdgeSize": "Randgrootte", + "coherenceMinDenoise": "Min. ontruising", + "cfgRescaleMultiplier": "Vermenigvuldiger voor CFG-herschaling" + }, + "settings": { + "models": "Modellen", + "displayInProgress": "Toon voortgangsafbeeldingen", + "confirmOnDelete": "Bevestig bij verwijderen", + "resetWebUI": "Herstel web-UI", + "resetWebUIDesc1": "Herstel web-UI herstelt alleen de lokale afbeeldingscache en de onthouden instellingen van je browser. Het verwijdert geen afbeeldingen van schijf.", + "resetWebUIDesc2": "Als afbeeldingen niet getoond worden in de galerij of iets anders werkt niet, probeer dan eerst deze herstelfunctie voordat je een fout aanmeldt op GitHub.", + "resetComplete": "Webinterface is hersteld.", + "developer": "Ontwikkelaar", + "general": "Algemeen", + "showProgressInViewer": "Toon voortgangsafbeeldingen in viewer", + "generation": "Genereren", + "ui": "Gebruikersinterface", + "antialiasProgressImages": "Voer anti-aliasing uit op voortgangsafbeeldingen", + "beta": "Bèta", + "clearIntermediates": "Wis tussentijdse afbeeldingen", + "clearIntermediatesDesc3": "Je galerijafbeeldingen zullen niet worden verwijderd.", + "clearIntermediatesWithCount_one": "Wis {{count}} tussentijdse afbeelding", + "clearIntermediatesWithCount_other": "Wis {{count}} tussentijdse afbeeldingen", + "clearIntermediatesDesc2": "Tussentijdse afbeeldingen zijn nevenproducten bij het genereren. Deze wijken af van de uitvoerafbeeldingen in de galerij. Als je tussentijdse afbeeldingen wist, wordt schijfruimte vrijgemaakt.", + "intermediatesCleared_one": "{{count}} tussentijdse afbeelding gewist", + "intermediatesCleared_other": "{{count}} tussentijdse afbeeldingen gewist", + "clearIntermediatesDesc1": "Als je tussentijdse afbeeldingen wist, dan wordt de staat hersteld van je canvas en van ControlNet.", + "intermediatesClearedFailed": "Fout bij wissen van tussentijdse afbeeldingen", + "clearIntermediatesDisabled": "Wachtrij moet leeg zijn om tussentijdse afbeeldingen te kunnen leegmaken", + "enableInformationalPopovers": "Schakel informatieve hulpballonnen in", + "enableInvisibleWatermark": "Schakel onzichtbaar watermerk in", + "enableNSFWChecker": "Schakel NSFW-controle in", + "reloadingIn": "Opnieuw laden na" + }, + "toast": { + "uploadFailed": "Upload mislukt", + "imageCopied": "Afbeelding gekopieerd", + "parametersNotSet": "Parameters niet ingesteld", + "serverError": "Serverfout", + "connected": "Verbonden met server", + "canceled": "Verwerking geannuleerd", + "uploadFailedInvalidUploadDesc": "Moet een enkele PNG- of JPEG-afbeelding zijn", + "parameterNotSet": "{{parameter}} niet ingesteld", + "parameterSet": "{{parameter}} ingesteld", + "problemCopyingImage": "Kan Afbeelding Niet Kopiëren", + "baseModelChangedCleared_one": "Basismodel is gewijzigd: {{count}} niet-compatibel submodel weggehaald of uitgeschakeld", + "baseModelChangedCleared_other": "Basismodel is gewijzigd: {{count}} niet-compatibele submodellen weggehaald of uitgeschakeld", + "loadedWithWarnings": "Werkstroom geladen met waarschuwingen", + "imageUploaded": "Afbeelding geüpload", + "addedToBoard": "Toegevoegd aan bord", + "workflowLoaded": "Werkstroom geladen", + "modelAddedSimple": "Model toegevoegd aan wachtrij", + "imageUploadFailed": "Fout bij uploaden afbeelding", + "workflowDeleted": "Werkstroom verwijderd", + "problemRetrievingWorkflow": "Fout bij ophalen van werkstroom", + "parameters": "Parameters", + "modelImportCanceled": "Importeren model geannuleerd", + "problemDeletingWorkflow": "Fout bij verwijderen van werkstroom", + "prunedQueue": "Wachtrij gesnoeid", + "problemDownloadingImage": "Fout bij downloaden afbeelding" + }, + "accessibility": { + "invokeProgressBar": "Voortgangsbalk Invoke", + "reset": "Herstel", + "uploadImage": "Upload afbeelding", + "previousImage": "Vorige afbeelding", + "nextImage": "Volgende afbeelding", + "menu": "Menu", + "about": "Over", + "mode": "Modus", + "resetUI": "$t(accessibility.reset) UI", + "createIssue": "Maak probleem aan" + }, + "nodes": { + "zoomOutNodes": "Uitzoomen", + "fitViewportNodes": "Aanpassen aan beeld", + "hideMinimapnodes": "Minimap verbergen", + "zoomInNodes": "Inzoomen", + "showMinimapnodes": "Minimap tonen", + "reloadNodeTemplates": "Herlaad knooppuntsjablonen", + "loadWorkflow": "Laad werkstroom", + "downloadWorkflow": "Download JSON van werkstroom", + "scheduler": "Planner", + "missingTemplate": "Ongeldig knooppunt: knooppunt {{node}} van het soort {{type}} heeft een ontbrekend sjabloon (niet geïnstalleerd?)", + "workflowDescription": "Korte beschrijving", + "noNodeSelected": "Geen knooppunt gekozen", + "addNode": "Voeg knooppunt toe", + "unableToValidateWorkflow": "Kan werkstroom niet valideren", + "enum": "Enumeratie", + "noOutputRecorded": "Geen uitvoer opgenomen", + "updateApp": "Werk app bij", + "colorCodeEdgesHelp": "Kleurgecodeerde randen op basis van hun verbonden velden", + "float": "Zwevende-kommagetal", + "workflowContact": "Contactpersoon", + "animatedEdges": "Geanimeerde randen", + "integer": "Geheel getal", + "nodeTemplate": "Sjabloon knooppunt", + "nodeOpacity": "Dekking knooppunt", + "snapToGrid": "Lijn uit op raster", + "nodeSearch": "Zoek naar knooppunten", + "updateNode": "Werk knooppunt bij", + "version": "Versie", + "validateConnections": "Valideer verbindingen en graaf", + "inputMayOnlyHaveOneConnection": "Invoer mag slechts een enkele verbinding hebben", + "notes": "Opmerkingen", + "nodeOutputs": "Uitvoer knooppunt", + "currentImageDescription": "Toont de huidige afbeelding in de knooppunteditor", + "validateConnectionsHelp": "Voorkom dat er ongeldige verbindingen worden gelegd en dat er ongeldige grafen worden aangeroepen", + "problemSettingTitle": "Fout bij instellen titel", + "ipAdapter": "IP-adapter", + "noConnectionInProgress": "Geen verbinding bezig te maken", + "workflowVersion": "Versie", + "fieldTypesMustMatch": "Veldsoorten moeten overeenkomen", + "workflow": "Werkstroom", + "edge": "Rand", + "animatedEdgesHelp": "Animeer gekozen randen en randen verbonden met de gekozen knooppunten", + "cannotDuplicateConnection": "Kan geen dubbele verbindingen maken", + "noWorkflow": "Geen werkstroom", + "workflowTags": "Labels", + "fullyContainNodesHelp": "Knooppunten moeten zich volledig binnen het keuzevak bevinden om te worden gekozen", + "workflowValidation": "Validatiefout werkstroom", + "nodeType": "Soort knooppunt", + "fullyContainNodes": "Omvat knooppunten volledig om ze te kiezen", + "executionStateInProgress": "Bezig", + "executionStateError": "Fout", + "boolean": "Booleaanse waarden", + "executionStateCompleted": "Voltooid", + "node": "Knooppunt", + "workflowAuthor": "Auteur", + "currentImage": "Huidige afbeelding", + "workflowName": "Naam", + "collection": "Verzameling", + "cannotConnectInputToInput": "Kan invoer niet aan invoer verbinden", + "workflowNotes": "Opmerkingen", + "string": "Tekenreeks", + "cannotConnectOutputToOutput": "Kan uitvoer niet aan uitvoer verbinden", + "connectionWouldCreateCycle": "Verbinding zou cyclisch worden", + "cannotConnectToSelf": "Kan niet aan zichzelf verbinden", + "notesDescription": "Voeg opmerkingen toe aan je werkstroom", + "unknownField": "Onbekend veld", + "colorCodeEdges": "Kleurgecodeerde randen", + "unknownNode": "Onbekend knooppunt", + "addNodeToolTip": "Voeg knooppunt toe (Shift+A, spatie)", + "loadingNodes": "Bezig met laden van knooppunten...", + "snapToGridHelp": "Lijn knooppunten uit op raster bij verplaatsing", + "workflowSettings": "Instellingen werkstroomeditor", + "nodePack": "Knooppuntpakket", + "sourceNodeFieldDoesNotExist": "Ongeldige rand: bron-/uitvoerveld {{node}}.{{field}} bestaat niet", + "collectionFieldType": "Verzameling {{name}}", + "deletedInvalidEdge": "Ongeldige hoek {{source}} -> {{target}} verwijderd", + "graph": "Grafiek", + "targetNodeDoesNotExist": "Ongeldige rand: doel-/invoerknooppunt {{node}} bestaat niet", + "resetToDefaultValue": "Herstel naar standaardwaarden", + "editMode": "Bewerk in Werkstroom-editor", + "showEdgeLabels": "Toon randlabels", + "showEdgeLabelsHelp": "Toon labels aan randen, waarmee de verbonden knooppunten mee worden aangegeven", + "clearWorkflowDesc2": "Je huidige werkstroom heeft niet-bewaarde wijzigingen.", + "unableToParseFieldType": "fout bij bepalen soort veld", + "sourceNodeDoesNotExist": "Ongeldige rand: bron-/uitvoerknooppunt {{node}} bestaat niet", + "unsupportedArrayItemType": "niet-ondersteunde soort van het array-onderdeel \"{{type}}\"", + "targetNodeFieldDoesNotExist": "Ongeldige rand: doel-/invoerveld {{node}}.{{field}} bestaat niet", + "newWorkflowDesc": "Een nieuwe werkstroom aanmaken?", + "collectionOrScalarFieldType": "Verzameling|scalair {{name}}", + "newWorkflow": "Nieuwe werkstroom", + "unknownErrorValidatingWorkflow": "Onbekende fout bij valideren werkstroom", + "unsupportedAnyOfLength": "te veel union-leden ({{count}})", + "viewMode": "Gebruik in lineaire weergave", + "unableToExtractSchemaNameFromRef": "fout bij het extraheren van de schemanaam via de ref", + "unsupportedMismatchedUnion": "niet-overeenkomende soort CollectionOrScalar met basissoorten {{firstType}} en {{secondType}}", + "unknownNodeType": "Onbekend soort knooppunt", + "edit": "Bewerk", + "updateAllNodes": "Werk knooppunten bij", + "allNodesUpdated": "Alle knooppunten bijgewerkt", + "nodeVersion": "Knooppuntversie", + "newWorkflowDesc2": "Je huidige werkstroom heeft niet-bewaarde wijzigingen.", + "clearWorkflow": "Maak werkstroom leeg", + "clearWorkflowDesc": "Deze werkstroom leegmaken en met een nieuwe beginnen?", + "inputFieldTypeParseError": "Fout bij bepalen van het soort invoerveld {{node}}.{{field}} ({{message}})", + "outputFieldTypeParseError": "Fout bij het bepalen van het soort uitvoerveld {{node}}.{{field}} ({{message}})", + "unableToExtractEnumOptions": "fout bij extraheren enumeratie-opties", + "unknownFieldType": "Soort $t(nodes.unknownField): {{type}}", + "unableToGetWorkflowVersion": "Fout bij ophalen schemaversie van werkstroom", + "betaDesc": "Deze uitvoering is in bèta. Totdat deze stabiel is kunnen er wijzigingen voorkomen gedurende app-updates die zaken kapotmaken. We zijn van plan om deze uitvoering op lange termijn te gaan ondersteunen.", + "prototypeDesc": "Deze uitvoering is een prototype. Er kunnen wijzigingen voorkomen gedurende app-updates die zaken kapotmaken. Deze kunnen op een willekeurig moment verwijderd worden.", + "noFieldsViewMode": "Deze werkstroom heeft geen geselecteerde velden om te tonen. Bekijk de volledige werkstroom om de waarden te configureren.", + "unableToUpdateNodes_one": "Fout bij bijwerken van {{count}} knooppunt", + "unableToUpdateNodes_other": "Fout bij bijwerken van {{count}} knooppunten" + }, + "dynamicPrompts": { + "seedBehaviour": { + "perPromptDesc": "Gebruik een verschillende seedwaarde per afbeelding", + "perIterationLabel": "Seedwaarde per iteratie", + "perIterationDesc": "Gebruik een verschillende seedwaarde per iteratie", + "perPromptLabel": "Seedwaarde per afbeelding", + "label": "Gedrag seedwaarde" + }, + "maxPrompts": "Max. prompts", + "dynamicPrompts": "Dynamische prompts", + "showDynamicPrompts": "Toon dynamische prompts", + "loading": "Genereren van dynamische prompts...", + "promptsPreview": "Voorvertoning prompts" + }, + "popovers": { + "noiseUseCPU": { + "paragraphs": [ + "Bepaalt of ruis wordt gegenereerd op de CPU of de GPU.", + "Met CPU-ruis ingeschakeld zal een bepaalde seedwaarde dezelfde afbeelding opleveren op welke machine dan ook.", + "Er is geen prestatieverschil bij het inschakelen van CPU-ruis." + ], + "heading": "Gebruik CPU-ruis" + }, + "paramScheduler": { + "paragraphs": [ + "De planner gebruikt gedurende het genereringsproces." + ], + "heading": "Planner" + }, + "scaleBeforeProcessing": { + "paragraphs": [ + "Schaalt het gekozen gebied naar de grootte die het meest geschikt is voor het model, vooraf aan het proces van het afbeeldingen genereren." + ], + "heading": "Schaal vooraf aan verwerking" + }, + "compositingMaskAdjustments": { + "heading": "Aanpassingen masker", + "paragraphs": [ + "Pas het masker aan." + ] + }, + "paramRatio": { + "heading": "Beeldverhouding", + "paragraphs": [ + "De beeldverhouding van de afmetingen van de afbeelding die wordt gegenereerd.", + "Een afbeeldingsgrootte (in aantal pixels) equivalent aan 512x512 wordt aanbevolen voor SD1.5-modellen. Een grootte-equivalent van 1024x1024 wordt aanbevolen voor SDXL-modellen." + ] + }, + "dynamicPrompts": { + "paragraphs": [ + "Dynamische prompts vormt een enkele prompt om in vele.", + "De basissyntax is \"a {red|green|blue} ball\". Dit zal de volgende drie prompts geven: \"a red ball\", \"a green ball\" en \"a blue ball\".", + "Gebruik de syntax zo vaak als je wilt in een enkele prompt, maar zorg ervoor dat het aantal gegenereerde prompts in lijn ligt met de instelling Max. prompts." + ], + "heading": "Dynamische prompts" + }, + "paramVAE": { + "paragraphs": [ + "Het model gebruikt voor het vertalen van AI-uitvoer naar de uiteindelijke afbeelding." + ], + "heading": "VAE" + }, + "paramIterations": { + "paragraphs": [ + "Het aantal te genereren afbeeldingen.", + "Als dynamische prompts is ingeschakeld, dan zal elke prompt dit aantal keer gegenereerd worden." + ], + "heading": "Iteraties" + }, + "paramVAEPrecision": { + "heading": "Nauwkeurigheid VAE", + "paragraphs": [ + "De nauwkeurigheid gebruikt tijdens de VAE-codering en -decodering. FP16/halve nauwkeurig is efficiënter, ten koste van kleine afbeeldingsvariaties." + ] + }, + "compositingCoherenceMode": { + "heading": "Modus", + "paragraphs": [ + "De modus van de coherentiefase." + ] + }, + "paramSeed": { + "paragraphs": [ + "Bepaalt de startruis die gebruikt wordt bij het genereren.", + "Schakel \"Willekeurige seedwaarde\" uit om identieke resultaten te krijgen met dezelfde genereer-instellingen." + ], + "heading": "Seedwaarde" + }, + "controlNetResizeMode": { + "heading": "Schaalmodus", + "paragraphs": [ + "Hoe de ControlNet-afbeelding zal worden geschaald aan de uitvoergrootte van de afbeelding." + ] + }, + "controlNetBeginEnd": { + "paragraphs": [ + "Op welke stappen van het ontruisingsproces ControlNet worden toegepast.", + "ControlNets die worden toegepast aan het begin begeleiden het compositieproces. ControlNets die worden toegepast aan het eind zorgen voor details." + ], + "heading": "Percentage begin- / eindstap" + }, + "dynamicPromptsSeedBehaviour": { + "paragraphs": [ + "Bepaalt hoe de seedwaarde wordt gebruikt bij het genereren van prompts.", + "Per iteratie zal een unieke seedwaarde worden gebruikt voor elke iteratie. Gebruik dit om de promptvariaties binnen een enkele seedwaarde te verkennen.", + "Bijvoorbeeld: als je vijf prompts heb, dan zal voor elke afbeelding dezelfde seedwaarde gebruikt worden.", + "De optie Per afbeelding zal een unieke seedwaarde voor elke afbeelding gebruiken. Dit biedt meer variatie." + ], + "heading": "Gedrag seedwaarde" + }, + "clipSkip": { + "paragraphs": [ + "Aantal over te slaan CLIP-modellagen.", + "Bepaalde modellen zijn beter geschikt met bepaalde Overslaan CLIP-instellingen." + ], + "heading": "Overslaan CLIP" + }, + "paramModel": { + "heading": "Model", + "paragraphs": [ + "Model gebruikt voor de ontruisingsstappen." + ] + }, + "compositingCoherencePass": { + "heading": "Coherentiefase", + "paragraphs": [ + "Een tweede ronde ontruising helpt bij het samenstellen van de erin- of eruitgetekende afbeelding." + ] + }, + "paramDenoisingStrength": { + "paragraphs": [ + "Hoeveel ruis wordt toegevoegd aan de invoerafbeelding.", + "0 levert een identieke afbeelding op, waarbij 1 een volledig nieuwe afbeelding oplevert." + ], + "heading": "Ontruisingssterkte" + }, + "paramNegativeConditioning": { + "paragraphs": [ + "Het genereerproces voorkomt de gegeven begrippen in de negatieve prompt. Gebruik dit om bepaalde zaken of voorwerpen uit te sluiten van de uitvoerafbeelding.", + "Ondersteunt Compel-syntax en -embeddingen." + ], + "heading": "Negatieve prompt" + }, + "compositingBlurMethod": { + "heading": "Vervagingsmethode", + "paragraphs": [ + "De methode van de vervaging die wordt toegepast op het gemaskeerd gebied." + ] + }, + "dynamicPromptsMaxPrompts": { + "heading": "Max. prompts", + "paragraphs": [ + "Beperkt het aantal prompts die kunnen worden gegenereerd door dynamische prompts." + ] + }, + "infillMethod": { + "paragraphs": [ + "Methode om een gekozen gebied in te vullen." + ], + "heading": "Invulmethode" + }, + "controlNetWeight": { + "heading": "Gewicht", + "paragraphs": [ + "Hoe sterk ControlNet effect heeft op de gegeneerde afbeelding." + ] + }, + "controlNet": { + "heading": "ControlNet", + "paragraphs": [ + "ControlNets begeleidt het genereerproces, waarbij geholpen wordt bij het maken van afbeeldingen met aangestuurde compositie, structuur of stijl, afhankelijk van het gekozen model." + ] + }, + "paramCFGScale": { + "heading": "CFG-schaal", + "paragraphs": [ + "Bepaalt hoeveel je prompt invloed heeft op het genereerproces." + ] + }, + "controlNetControlMode": { + "paragraphs": [ + "Geeft meer gewicht aan ofwel de prompt danwel ControlNet." + ], + "heading": "Controlemodus" + }, + "paramSteps": { + "heading": "Stappen", + "paragraphs": [ + "Het aantal uit te voeren stappen tijdens elke generatie.", + "Een hoger aantal stappen geven meestal betere afbeeldingen, ten koste van een hogere benodigde tijd om te genereren." + ] + }, + "paramPositiveConditioning": { + "heading": "Positieve prompt", + "paragraphs": [ + "Begeleidt het generartieproces. Gebruik een woord of frase naar keuze.", + "Syntaxes en embeddings voor Compel en dynamische prompts." + ] + }, + "lora": { + "heading": "Gewicht LoRA", + "paragraphs": [ + "Een hogere LoRA-gewicht zal leiden tot een groter effect op de uiteindelijke afbeelding." + ] + } + }, + "metadata": { + "positivePrompt": "Positieve prompt", + "negativePrompt": "Negatieve prompt", + "generationMode": "Genereermodus", + "Threshold": "Drempelwaarde ruis", + "metadata": "Metagegevens", + "strength": "Sterkte Afbeelding naar afbeelding", + "seed": "Seedwaarde", + "imageDetails": "Afbeeldingsdetails", + "model": "Model", + "noImageDetails": "Geen afbeeldingsdetails gevonden", + "cfgScale": "CFG-schaal", + "recallParameters": "Opnieuw aan te roepen parameters", + "height": "Hoogte", + "noMetaData": "Geen metagegevens gevonden", + "width": "Breedte", + "createdBy": "Gemaakt door", + "workflow": "Werkstroom", + "steps": "Stappen", + "scheduler": "Planner", + "noRecallParameters": "Geen opnieuw uit te voeren parameters gevonden" + }, + "queue": { + "status": "Status", + "pruneSucceeded": "{{item_count}} voltooide onderdelen uit wachtrij opgeruimd", + "cancelTooltip": "Annuleer huidig onderdeel", + "queueEmpty": "Wachtrij leeg", + "pauseSucceeded": "Verwerker onderbroken", + "in_progress": "Bezig", + "queueFront": "Voeg vooraan toe in wachtrij", + "notReady": "Fout bij plaatsen in wachtrij", + "batchFailedToQueue": "Fout bij reeks in wachtrij plaatsen", + "completed": "Voltooid", + "queueBack": "Voeg toe aan wachtrij", + "cancelFailed": "Fout bij annuleren onderdeel", + "batchQueued": "Reeks in wachtrij geplaatst", + "pauseFailed": "Fout bij onderbreken verwerker", + "clearFailed": "Fout bij wissen van wachtrij", + "front": "begin", + "clearSucceeded": "Wachtrij gewist", + "pause": "Onderbreek", + "pruneTooltip": "Ruim {{item_count}} voltooide onderdelen op", + "cancelSucceeded": "Onderdeel geannuleerd", + "batchQueuedDesc_one": "Voeg {{count}} sessie toe aan het {{direction}} van de wachtrij", + "batchQueuedDesc_other": "Voeg {{count}} sessies toe aan het {{direction}} van de wachtrij", + "graphQueued": "Graaf in wachtrij geplaatst", + "queue": "Wachtrij", + "batch": "Reeks", + "clearQueueAlertDialog": "Als je de wachtrij onmiddellijk wist, dan worden alle onderdelen die bezig zijn geannuleerd en wordt de wachtrij volledig gewist.", + "pending": "Wachtend", + "completedIn": "Voltooid na", + "resumeFailed": "Fout bij hervatten verwerker", + "clear": "Wis", + "prune": "Ruim op", + "total": "Totaal", + "canceled": "Geannuleerd", + "pruneFailed": "Fout bij opruimen van wachtrij", + "cancelBatchSucceeded": "Reeks geannuleerd", + "clearTooltip": "Annuleer en wis alle onderdelen", + "current": "Huidig", + "pauseTooltip": "Onderbreek verwerker", + "failed": "Mislukt", + "cancelItem": "Annuleer onderdeel", + "next": "Volgende", + "cancelBatch": "Annuleer reeks", + "back": "eind", + "cancel": "Annuleer", + "session": "Sessie", + "resumeSucceeded": "Verwerker hervat", + "enqueueing": "Bezig met toevoegen van reeks aan wachtrij", + "resumeTooltip": "Hervat verwerker", + "resume": "Hervat", + "cancelBatchFailed": "Fout bij annuleren van reeks", + "clearQueueAlertDialog2": "Weet je zeker dat je de wachtrij wilt wissen?", + "item": "Onderdeel", + "graphFailedToQueue": "Fout bij toevoegen graaf aan wachtrij" + }, + "sdxl": { + "refinerStart": "Startwaarde verfijning", + "scheduler": "Planner", + "cfgScale": "CFG-schaal", + "noModelsAvailable": "Geen modellen beschikbaar", + "refiner": "Verfijning", + "negAestheticScore": "Negatieve esthetische score", + "denoisingStrength": "Sterkte ontruising", + "refinermodel": "Verfijningsmodel", + "posAestheticScore": "Positieve esthetische score", + "loading": "Bezig met laden...", + "steps": "Stappen", + "refinerSteps": "Aantal stappen verfijner" + }, + "models": { + "noMatchingModels": "Geen overeenkomend modellen", + "loading": "bezig met laden", + "noModelsAvailable": "Geen modellen beschikbaar", + "selectModel": "Kies een model", + "noRefinerModelsInstalled": "Geen SDXL-verfijningsmodellen geïnstalleerd", + "defaultVAE": "Standaard-VAE", + "lora": "LoRA", + "addLora": "Voeg LoRA toe", + "concepts": "Concepten" + }, + "boards": { + "autoAddBoard": "Voeg automatisch bord toe", + "topMessage": "Dit bord bevat afbeeldingen die in gebruik zijn door de volgende functies:", + "move": "Verplaats", + "menuItemAutoAdd": "Voeg dit automatisch toe aan bord", + "myBoard": "Mijn bord", + "searchBoard": "Zoek borden...", + "noMatching": "Geen overeenkomende borden", + "selectBoard": "Kies een bord", + "cancel": "Annuleer", + "addBoard": "Voeg bord toe", + "bottomMessage": "Als je dit bord en alle afbeeldingen erop verwijdert, dan worden alle functies teruggezet die ervan gebruik maken.", + "uncategorized": "Zonder categorie", + "downloadBoard": "Download bord", + "changeBoard": "Wijzig bord", + "loading": "Bezig met laden...", + "clearSearch": "Maak zoekopdracht leeg", + "deleteBoard": "Verwijder bord", + "deleteBoardAndImages": "Verwijder bord en afbeeldingen", + "deleteBoardOnly": "Verwijder alleen bord", + "deletedBoardsCannotbeRestored": "Verwijderde borden kunnen niet worden hersteld", + "movingImagesToBoard_one": "Verplaatsen van {{count}} afbeelding naar bord:", + "movingImagesToBoard_other": "Verplaatsen van {{count}} afbeeldingen naar bord:" + }, + "invocationCache": { + "disable": "Schakel uit", + "misses": "Mislukt cacheverzoek", + "enableFailed": "Fout bij inschakelen aanroepcache", + "invocationCache": "Aanroepcache", + "clearSucceeded": "Aanroepcache gewist", + "enableSucceeded": "Aanroepcache ingeschakeld", + "clearFailed": "Fout bij wissen aanroepcache", + "hits": "Gelukt cacheverzoek", + "disableSucceeded": "Aanroepcache uitgeschakeld", + "disableFailed": "Fout bij uitschakelen aanroepcache", + "enable": "Schakel in", + "clear": "Wis", + "maxCacheSize": "Max. grootte cache", + "cacheSize": "Grootte cache" + }, + "accordions": { + "generation": { + "title": "Genereren" + }, + "image": { + "title": "Afbeelding" + }, + "advanced": { + "title": "Geavanceerd", + "options": "$t(accordions.advanced.title) Opties" + }, + "control": { + "title": "Besturing" + }, + "compositing": { + "title": "Samenstellen", + "coherenceTab": "Coherentiefase", + "infillTab": "Invullen" + } + }, + "hrf": { + "metadata": { + "strength": "Sterkte oplossing voor hoge resolutie", + "method": "Methode oplossing voor hoge resolutie", + "enabled": "Oplossing voor hoge resolutie ingeschakeld" + }, + "hrf": "Oplossing voor hoge resolutie" + }, + "prompt": { + "addPromptTrigger": "Voeg prompttrigger toe", + "compatibleEmbeddings": "Compatibele embeddings" + } +} diff --git a/invokeai/frontend/web/public/locales/pl.json b/invokeai/frontend/web/public/locales/pl.json new file mode 100644 index 00000000000..49cf5a24c43 --- /dev/null +++ b/invokeai/frontend/web/public/locales/pl.json @@ -0,0 +1,315 @@ +{ + "common": { + "hotkeysLabel": "Skróty klawiszowe", + "languagePickerLabel": "Język", + "reportBugLabel": "Zgłoś błąd", + "settingsLabel": "Ustawienia", + "img2img": "Obraz na obraz", + "nodes": "Węzły", + "upload": "Prześlij", + "load": "Załaduj", + "statusDisconnected": "Odłączono", + "githubLabel": "GitHub", + "discordLabel": "Discord", + "clipboard": "Schowek", + "aboutDesc": "Wykorzystujesz Invoke do pracy? Sprawdź:", + "ai": "SI", + "areYouSure": "Czy jesteś pewien?", + "copyError": "$t(gallery.copy) Błąd", + "apply": "Zastosuj", + "copy": "Kopiuj", + "or": "albo", + "add": "Dodaj", + "off": "Wyłączony", + "accept": "Zaakceptuj", + "cancel": "Anuluj", + "advanced": "Zawansowane", + "back": "Do tyłu", + "auto": "Automatyczny", + "beta": "Beta", + "close": "Wyjdź", + "checkpoint": "Punkt kontrolny", + "controlNet": "ControlNet", + "details": "Detale", + "direction": "Kierunek", + "ipAdapter": "Adapter IP", + "dontAskMeAgain": "Nie pytaj ponownie", + "modelManager": "Menedżer modeli", + "blue": "Niebieski", + "orderBy": "Sortuj według", + "openInNewTab": "Otwórz w nowym oknie", + "somethingWentWrong": "Coś poszło nie tak", + "green": "Zielony", + "red": "Czerwony", + "saveAs": "Zapisz jako", + "outputs": "Wyjścia", + "data": "Dane", + "t2iAdapter": "Adapter T2I", + "selected": "Zaznaczone", + "warnings": "Ostrzeżenia", + "save": "Zapisz", + "created": "Stworzono", + "alpha": "Alfa", + "error": "Bład", + "editor": "Edytor", + "loading": "Ładuję", + "edit": "Edytuj", + "enabled": "Aktywny", + "communityLabel": "Społeczeństwo", + "linear": "Liniowy", + "installed": "Zainstalowany", + "dontShowMeThese": "Nie pokazuj mi tego", + "openInViewer": "Otwórz podgląd", + "safetensors": "Bezpieczniki", + "ok": "Ok", + "loadingImage": "wczytywanie zdjęcia", + "input": "Wejście", + "view": "Podgląd", + "learnMore": "Dowiedz się więcej", + "loadingModel": "Wczytywanie modelu", + "postprocessing": "Przetwarzanie końcowe", + "random": "Losowo", + "disabled": "Wyłączony", + "generating": "Generowanie", + "simple": "Prosty", + "folder": "Katalog", + "format": "Format", + "updated": "Zaktualizowano", + "unknown": "nieznany", + "delete": "Usuń", + "template": "Szablon", + "txt2img": "Tekst na obraz", + "file": "Plik", + "toResolve": "Do rozwiązania", + "unknownError": "Nieznany błąd", + "placeholderSelectAModel": "Wybierz model", + "new": "Nowy", + "none": "Żadne", + "reset": "Reset", + "on": "Włączony", + "aboutHeading": "Posiadaj swoją kreatywną moc" + }, + "gallery": { + "galleryImageSize": "Rozmiar obrazów", + "gallerySettings": "Ustawienia galerii", + "autoSwitchNewImages": "Przełączaj na nowe obrazy", + "gallery": "Galeria", + "alwaysShowImageSizeBadge": "Zawsze pokazuj odznakę wielkości obrazu", + "assetsTab": "Pliki, które wrzuciłeś do użytku w twoich projektach.", + "currentlyInUse": "Ten obraz jest obecnie w użyciu przez następujące funkcje:", + "boardsSettings": "Ustawienia tablic", + "autoAssignBoardOnClick": "Automatycznie przypisz tablicę po kliknięciu", + "copy": "Kopiuj" + }, + "parameters": { + "images": "L. obrazów", + "steps": "L. kroków", + "cfgScale": "Skala CFG", + "width": "Szerokość", + "height": "Wysokość", + "seed": "Inicjator", + "shuffle": "Losuj", + "noiseThreshold": "Poziom szumu", + "perlinNoise": "Szum Perlina", + "type": "Metoda", + "strength": "Siła", + "upscaling": "Powiększanie", + "scale": "Skala", + "imageFit": "Przeskaluj oryginalny obraz", + "scaleBeforeProcessing": "Tryb skalowania", + "scaledWidth": "Sk. do szer.", + "scaledHeight": "Sk. do wys.", + "infillMethod": "Metoda wypełniania", + "tileSize": "Rozmiar kafelka", + "usePrompt": "Skopiuj sugestie", + "useSeed": "Skopiuj inicjator", + "useAll": "Skopiuj wszystko", + "info": "Informacje" + }, + "settings": { + "models": "Modele", + "displayInProgress": "Podgląd generowanego obrazu", + "confirmOnDelete": "Potwierdzaj usuwanie", + "resetWebUI": "Zresetuj interfejs", + "resetWebUIDesc1": "Resetowanie interfejsu wyczyści jedynie dane i ustawienia zapisane w pamięci przeglądarki. Nie usunie żadnych obrazów z dysku.", + "resetWebUIDesc2": "Jeśli obrazy nie są poprawnie wyświetlane w galerii lub doświadczasz innych problemów, przed zgłoszeniem błędu spróbuj zresetować interfejs.", + "resetComplete": "Interfejs został zresetowany. Odśwież stronę, aby załadować ponownie." + }, + "toast": { + "uploadFailed": "Błąd przesyłania obrazu", + "imageCopied": "Skopiowano obraz", + "parametersNotSet": "Nie ustawiono parametrów" + }, + "accessibility": { + "invokeProgressBar": "Pasek postępu", + "reset": "Zerowanie", + "uploadImage": "Wgrywanie obrazu", + "previousImage": "Poprzedni obraz", + "nextImage": "Następny obraz", + "menu": "Menu", + "mode": "Tryb", + "resetUI": "$t(accessibility.reset) UI", + "uploadImages": "Wgrywaj obrazy", + "about": "Informacje", + "toggleRightPanel": "Przełącz prawy panel (G)", + "toggleLeftPanel": "Przełącz lewy panel (G)", + "createIssue": "Stwórz problem", + "submitSupportTicket": "Wyślij bilet pomocy" + }, + "boards": { + "cancel": "Anuluj", + "noBoards": "Brak tablic typu {{boardType}}", + "imagesWithCount_one": "{{count}} zdjęcie", + "imagesWithCount_few": "{{count}} zdjęcia", + "imagesWithCount_many": "{{count}} zdjęcia", + "private": "Prywatne tablice", + "updateBoardError": "Błąd aktualizacji tablicy", + "uncategorized": "Nieskategoryzowane", + "selectBoard": "Wybierz tablicę", + "downloadBoard": "Pobierz tablice", + "loading": "Ładowanie...", + "move": "Przenieś", + "noMatching": "Brak pasujących tablic", + "addBoard": "Dodaj tablicę", + "autoAddBoard": "Automatycznie dodaj tablicę", + "searchBoard": "Szukaj tablic.", + "unarchiveBoard": "Odarchiwizuj tablicę", + "selectedForAutoAdd": "Wybrany do automatycznego dodania", + "deleteBoard": "Usuń tablicę", + "clearSearch": "Usuń historię", + "addSharedBoard": "Dodaj udostępnioną tablicę", + "boards": "Tablice", + "addPrivateBoard": "Dodaj prywatną tablicę", + "movingImagesToBoard_one": "Przenoszenie {{count}} zdjęcia do tablicy:", + "movingImagesToBoard_few": "Przenoszenie {{count}} zdjęć do tablicy:", + "movingImagesToBoard_many": "Przenoszenie {{count}} zdjęć do tablicy:", + "shared": "Udostępnione tablice", + "topMessage": "Ta tablica zawiera obrazy wykorzystywane w następujących funkcjach:", + "deletedPrivateBoardsCannotbeRestored": "Usunięte tablice nie mogą być odzyskane. Wybierając \"Usuń tylko tablicę\" spowoduje że obrazy zostaną przeniesione do prywatnego nieskategoryzowanego stanu autora obrazu.", + "changeBoard": "Zmień tablicę", + "bottomMessage": "Usuwając tę tablicę oraz jej obrazów zresetują wszystkie funkcje które obecnie ich używają.", + "deleteBoardAndImages": "Usuń tablicę i zdjęcia", + "deleteBoardOnly": "Usuń tylko tablicę", + "deletedBoardsCannotbeRestored": "Usunięte tablice nie mogą być odzyskane. Wybierając \"Usuń tylko tablicę\" spowoduje że obrazy zostaną przeniesione do nieskategoryzowanego stanu.", + "archiveBoard": "Zarchiwizuj tablicę", + "archived": "Zarchiwizowano", + "myBoard": "Moja tablica", + "menuItemAutoAdd": "Automatycznie dodaj do tej tablicy" + }, + "accordions": { + "compositing": { + "title": "Kompozycja", + "infillTab": "Inskrypcja", + "coherenceTab": "Przebieg Koherencji" + }, + "generation": { + "title": "Generowanie" + }, + "image": { + "title": "Zdjęcie" + }, + "advanced": { + "options": "$t(accordions.advanced.title) Opcje", + "title": "Zaawansowane" + }, + "control": { + "title": "Kontrola" + } + }, + "hrf": { + "metadata": { + "enabled": "Włączono poprawkę wysokiej rozdzielczości", + "strength": "Moc poprawki wysokiej rozdzielczości", + "method": "Metoda High Resolution Fix" + }, + "hrf": "Poprawka \"Wysoka rozdzielczość\"" + }, + "queue": { + "cancelTooltip": "Anuluj aktualną pozycję", + "resumeFailed": "Błąd z kontynuowaniem procesora", + "current": "Obecne", + "cancelBatchFailed": "Problem z anulacją masy", + "queueFront": "Dodaj do przodu kolejki", + "cancelBatch": "Anuluj serię", + "cancelFailed": "Problem z anulowaniem pozycji", + "pruneTooltip": "Wyczyść {{item_count}} skończonych pozycji", + "pruneSucceeded": "Wyczyszczono {{item_count}} zakończonych pozycji z kolejki", + "cancelBatchSucceeded": "Partia anulowana", + "clear": "Wyczyść", + "clearTooltip": "Anuluj i usuń wszystkie pozycje", + "clearSucceeded": "Kolejka wyczyszczona", + "cancelItem": "Anuluj pozycję", + "clearQueueAlertDialog2": "Czy na pewno chcesz wyczyścić kolejkę?", + "pauseFailed": "Problem z zapauzowaniem processora", + "clearFailed": "Problem z czyszczeniem kolejki", + "queueBack": "Dodaj do kolejki", + "queueEmpty": "Kolejka pusta", + "enqueueing": "Kolejkowanie partii", + "resumeTooltip": "Kontynuuj processor", + "resumeSucceeded": "Processor kontynuowany", + "pause": "Zapauzuj", + "pauseTooltip": "Zapauzuj processor", + "queue": "Kolejka", + "resume": "Kontynuuj", + "cancel": "Anuluj", + "cancelSucceeded": "Pozycja anulowana", + "prune": "Wyczyść", + "pauseSucceeded": "Processor zapauzowany", + "clearQueueAlertDialog": "Czyszczenie kolejki od razu anuluje wszystkie przetwarzane elementy and całkowicie czyści kolejkę. Oczekujące filtry zostaną anulowane.", + "pruneFailed": "Problem z wyczyszczeniem kolejki", + "batchQueued": "Masa w kolejce", + "openQueue": "Otwórz kolejkę", + "iterations_one": "Iteracja", + "iterations_few": "Iteracje", + "iterations_many": "Iteracje", + "graphQueued": "Wykres w kolejce", + "canvas": "Płótno", + "generation": "Generacja", + "status": "Status", + "total": "Suma", + "time": "Czas", + "front": "Przód", + "back": "tył", + "batchFailedToQueue": "Nie można zkolejkować masy", + "completedIn": "Ukończony w całości", + "other": "Inne", + "origin": "Pochodzenie", + "destination": "Miejsce docelowe", + "notReady": "Nie można zkolejkować", + "canceled": "Anulowano", + "in_progress": "W trakcie", + "gallery": "Galeria", + "session": "Sesja", + "pending": "W toku", + "completed": "Zakończono", + "item": "Pozycja", + "failed": "Niepowodzenie", + "graphFailedToQueue": "NIe udało się dodać tabeli do kolejki", + "workflows": "Przepływy pracy", + "next": "Następny", + "batchQueuedDesc_one": "Dodano {{count}} sesję do {{direction}} kolejki", + "batchQueuedDesc_few": "Dodano {{count}} sesje do {{direction}} kolejki", + "batchQueuedDesc_many": "Dodano {{count}} sesje do {{direction}} kolejki", + "batch": "Masa", + "upscaling": "Skalowanie w górę", + "generations_one": "Generacja", + "generations_few": "Generacje", + "generations_many": "Generacje", + "prompts_one": "Monit", + "prompts_few": "Monity", + "prompts_many": "Monity", + "batchSize": "Rozmiar masy" + }, + "prompt": { + "compatibleEmbeddings": "Kompatybilne osadzenia", + "noMatchingTriggers": "Nie dopasowywanie spustów" + }, + "invocationCache": { + "hits": "Uderzenia cache", + "enable": "Włącz", + "clear": "Wyczyść", + "disable": "Wyłącz", + "maxCacheSize": "Maksymalny rozmiar cache", + "cacheSize": "Rozmiar Cache" + } +} diff --git a/invokeai/frontend/web/public/locales/pt-BR.json b/invokeai/frontend/web/public/locales/pt-BR.json new file mode 100644 index 00000000000..fd77dd3ea87 --- /dev/null +++ b/invokeai/frontend/web/public/locales/pt-BR.json @@ -0,0 +1,99 @@ +{ + "common": { + "hotkeysLabel": "Teclas de atalho", + "languagePickerLabel": "Seletor de Idioma", + "reportBugLabel": "Relatar Bug", + "settingsLabel": "Configurações", + "img2img": "Imagem Para Imagem", + "nodes": "Nódulos", + "upload": "Enviar", + "load": "Carregar", + "statusDisconnected": "Disconectado", + "githubLabel": "Github", + "discordLabel": "Discord", + "back": "Voltar", + "loading": "Carregando" + }, + "gallery": { + "galleryImageSize": "Tamanho da Imagem", + "gallerySettings": "Configurações de Galeria", + "autoSwitchNewImages": "Trocar para Novas Imagens Automaticamente" + }, + "modelManager": { + "modelManager": "Gerente de Modelo", + "model": "Modelo", + "modelUpdated": "Modelo Atualizado", + "manual": "Manual", + "name": "Nome", + "description": "Descrição", + "config": "Configuração", + "width": "Largura", + "height": "Altura", + "addModel": "Adicionar Modelo", + "availableModels": "Modelos Disponíveis", + "search": "Procurar", + "load": "Carregar", + "active": "Ativado", + "selected": "Selecionada", + "delete": "Excluir", + "deleteModel": "Excluir modelo", + "deleteConfig": "Excluir Config", + "deleteMsg1": "Tem certeza de que deseja excluir esta entrada do modelo de InvokeAI?", + "deleteMsg2": "Isso não vai excluir o arquivo de modelo checkpoint do seu disco. Você pode lê-los, se desejar.", + "repo_id": "Repo ID", + "convertToDiffusers": "Converter para Diffusers", + "convertToDiffusersHelpText1": "Este modelo será convertido para o formato 🧨 Diffusers.", + "convertToDiffusersHelpText5": "Por favor, certifique-se de que você tenha espaço suficiente em disco. Os modelos geralmente variam entre 4GB e 7GB de tamanho.", + "convertToDiffusersHelpText6": "Você deseja converter este modelo?", + "convertToDiffusersHelpText3": "Seu arquivo de ponto de verificação no disco NÃO será excluído ou modificado de forma alguma. Você pode adicionar seu ponto de verificação ao Gerenciador de modelos novamente, se desejar.", + "convertToDiffusersHelpText4": "Este é um processo único. Pode levar cerca de 30 a 60s, dependendo das especificações do seu computador.", + "modelConverted": "Modelo Convertido", + "alpha": "Alpha", + "allModels": "Todos os Modelos", + "convert": "Converter", + "convertToDiffusersHelpText2": "Este processo irá substituir sua entrada de Gerenciador de Modelos por uma versão Diffusers do mesmo modelo." + }, + "parameters": { + "images": "Imagems", + "steps": "Passos", + "cfgScale": "Escala CFG", + "width": "Largura", + "height": "Altura", + "seed": "Seed", + "shuffle": "Embaralhar", + "noiseThreshold": "Limite de Ruído", + "perlinNoise": "Ruído de Perlin", + "type": "Tipo", + "strength": "Força", + "upscaling": "Redimensionando", + "scale": "Escala", + "imageFit": "Caber Imagem Inicial No Tamanho de Saída", + "scaleBeforeProcessing": "Escala Antes do Processamento", + "scaledWidth": "L Escalada", + "scaledHeight": "A Escalada", + "infillMethod": "Método de Preenchimento", + "tileSize": "Tamanho do Ladrilho", + "usePrompt": "Usar Prompt", + "useSeed": "Usar Seed", + "useAll": "Usar Todos", + "info": "Informações", + "symmetry": "Simetria", + "copyImage": "Copiar imagem", + "denoisingStrength": "A força de remoção de ruído", + "general": "Geral" + }, + "settings": { + "models": "Modelos", + "displayInProgress": "Mostrar Progresso de Imagens Em Andamento", + "confirmOnDelete": "Confirmar Antes de Apagar", + "resetWebUI": "Reiniciar Interface", + "resetWebUIDesc1": "Reiniciar a interface apenas reinicia o cache local do broswer para imagens e configurações lembradas. Não apaga nenhuma imagem do disco.", + "resetWebUIDesc2": "Se as imagens não estão aparecendo na galeria ou algo mais não está funcionando, favor tentar reiniciar antes de postar um problema no GitHub.", + "resetComplete": "A interface foi reiniciada. Atualize a página para carregar." + }, + "toast": { + "uploadFailed": "Envio Falhou", + "imageCopied": "Imagem Copiada", + "parametersNotSet": "Parâmetros Não Definidos" + } +} diff --git a/invokeai/frontend/web/public/locales/pt.json b/invokeai/frontend/web/public/locales/pt.json new file mode 100644 index 00000000000..f24022363ad --- /dev/null +++ b/invokeai/frontend/web/public/locales/pt.json @@ -0,0 +1,123 @@ +{ + "common": { + "reportBugLabel": "Reportar Bug", + "settingsLabel": "Configurações", + "languagePickerLabel": "Seletor de Idioma", + "hotkeysLabel": "Hotkeys", + "img2img": "Imagem para Imagem", + "nodes": "Nós", + "upload": "Upload", + "load": "Abrir", + "back": "Voltar", + "statusDisconnected": "Desconectado", + "githubLabel": "Github", + "discordLabel": "Discord", + "loading": "A carregar" + }, + "gallery": { + "gallerySettings": "Configurações de Galeria", + "autoSwitchNewImages": "Trocar para Novas Imagens Automaticamente", + "galleryImageSize": "Tamanho da Imagem" + }, + "modelManager": { + "modelUpdated": "Modelo Atualizado", + "description": "Descrição", + "repo_id": "Repo ID", + "width": "Largura", + "height": "Altura", + "deleteConfig": "Apagar Config", + "convertToDiffusersHelpText6": "Deseja converter este modelo?", + "alpha": "Alpha", + "config": "Configuração", + "modelConverted": "Modelo Convertido", + "manual": "Manual", + "name": "Nome", + "availableModels": "Modelos Disponíveis", + "load": "Carregar", + "active": "Ativado", + "deleteModel": "Apagar modelo", + "deleteMsg1": "Tem certeza de que deseja apagar esta entrada do modelo de InvokeAI?", + "deleteMsg2": "Isso não vai apagar o ficheiro de modelo checkpoint do seu disco. Pode lê-los, se desejar.", + "convertToDiffusers": "Converter para Diffusers", + "convertToDiffusersHelpText1": "Este modelo será convertido ao formato 🧨 Diffusers.", + "convertToDiffusersHelpText2": "Este processo irá substituir a sua entrada de Gestor de Modelos por uma versão Diffusers do mesmo modelo.", + "convertToDiffusersHelpText3": "O seu ficheiro de ponto de verificação no disco NÃO será excluído ou modificado de forma alguma. Pode adicionar o seu ponto de verificação ao Gestor de modelos novamente, se desejar.", + "none": "nenhum", + "modelManager": "Gerente de Modelo", + "model": "Modelo", + "allModels": "Todos os Modelos", + "addModel": "Adicionar Modelo", + "search": "Procurar", + "selected": "Selecionada", + "delete": "Apagar", + "convert": "Converter", + "convertToDiffusersHelpText4": "Este é um processo único. Pode levar cerca de 30 a 60s, a depender das especificações do seu computador.", + "convertToDiffusersHelpText5": "Por favor, certifique-se de que tenha espaço suficiente no disco. Os modelos geralmente variam entre 4GB e 7GB de tamanho." + }, + "parameters": { + "width": "Largura", + "seed": "Seed", + "general": "Geral", + "shuffle": "Embaralhar", + "noiseThreshold": "Limite de Ruído", + "perlinNoise": "Ruído de Perlin", + "type": "Tipo", + "denoisingStrength": "A força de remoção de ruído", + "scale": "Escala", + "imageFit": "Caber Imagem Inicial No Tamanho de Saída", + "tileSize": "Tamanho do Ladrilho", + "symmetry": "Simetria", + "usePrompt": "Usar Prompt", + "strength": "Força", + "upscaling": "Redimensionando", + "scaleBeforeProcessing": "Escala Antes do Processamento", + "images": "Imagems", + "steps": "Passos", + "cfgScale": "Escala CFG", + "height": "Altura", + "scaledWidth": "L Escalada", + "scaledHeight": "A Escalada", + "infillMethod": "Método de Preenchimento", + "copyImage": "Copiar imagem", + "useSeed": "Usar Seed", + "useAll": "Usar Todos", + "info": "Informações" + }, + "settings": { + "confirmOnDelete": "Confirmar Antes de Apagar", + "resetWebUIDesc1": "Reiniciar a interface apenas reinicia o cache local do broswer para imagens e configurações lembradas. Não apaga nenhuma imagem do disco.", + "models": "Modelos", + "displayInProgress": "Mostrar Progresso de Imagens Em Andamento", + "resetWebUI": "Reiniciar Interface", + "resetWebUIDesc2": "Se as imagens não estão a aparecer na galeria ou algo mais não está a funcionar, favor tentar reiniciar antes de postar um problema no GitHub.", + "resetComplete": "A interface foi reiniciada. Atualize a página para carregar." + }, + "toast": { + "uploadFailed": "Envio Falhou", + "imageCopied": "Imagem Copiada", + "parametersNotSet": "Parâmetros Não Definidos" + }, + "accessibility": { + "invokeProgressBar": "Invocar barra de progresso", + "reset": "Reiniciar", + "nextImage": "Próxima imagem", + "uploadImage": "Enviar imagem", + "previousImage": "Imagem Anterior", + "menu": "Menu", + "about": "Sobre", + "resetUI": "$t(accessibility.reset)UI", + "createIssue": "Reportar Problema", + "submitSupportTicket": "Submeter um ticket de Suporte", + "mode": "Modo" + }, + "boards": { + "selectedForAutoAdd": "Selecionado para Auto-Adicionar", + "addBoard": "Adicionar Quadro", + "addPrivateBoard": "Adicionar Quadro privado", + "addSharedBoard": "Adicionar quadro Compartilhado", + "boards": "Quadros", + "autoAddBoard": "Auto-adicao de Quadro", + "archiveBoard": "Arquivar Quadro", + "archived": "Arquivado" + } +} diff --git a/invokeai/frontend/web/public/locales/ro.json b/invokeai/frontend/web/public/locales/ro.json new file mode 100644 index 00000000000..9fb4068a93f --- /dev/null +++ b/invokeai/frontend/web/public/locales/ro.json @@ -0,0 +1,457 @@ +{ + "accessibility": { + "about": "Despre", + "reset": "Resetează", + "menu": "Meniu", + "mode": "Mod" + }, + "common": { + "hotkeysLabel": "Scurtături", + "languagePickerLabel": "Limbă", + "githubLabel": "Github", + "discordLabel": "Discord", + "settingsLabel": "Setări", + "nodes": "Workflow-uri", + "upload": "Încarcă", + "load": "Încarcă", + "back": "Înapoi", + "statusDisconnected": "Deconectat", + "loading": "Se încarcă", + "cancel": "Anulează", + "accept": "Acceptă", + "linear": "Linear", + "random": "Random", + "communityLabel": "Comunitate", + "advanced": "Avansat", + "controlNet": "ControlNet", + "auto": "Auto", + "on": "Pornit", + "checkpoint": "Checkpoint", + "data": "Date", + "details": "Detalii", + "inpaint": "inpaint", + "outpaint": "outpaint", + "outputs": "Outputs", + "safetensors": "Safetensors", + "simple": "Simplu", + "template": "Șablon", + "ai": "ai", + "error": "Eroare", + "file": "Fișier", + "folder": "Folder", + "format": "format", + "input": "Input", + "installed": "Instalat", + "unknown": "Necunoscut", + "delete": "Șterge", + "direction": "Direcție", + "save": "Salvează", + "updated": "Actualizat", + "created": "Creat", + "or": "sau", + "red": "Roșu", + "green": "Verde", + "blue": "Albastru", + "alpha": "Alpha", + "copy": "Copiază", + "add": "Adaugă", + "beta": "Beta", + "selected": "Selectat", + "editor": "Editor", + "tab": "Filă", + "enabled": "Activat", + "disabled": "Dezactivat", + "apply": "Aplică", + "view": "Vizualizează", + "edit": "Editează", + "off": "Oprit", + "reset": "Resetează", + "none": "Niciunul", + "new": "Nou" + }, + "modelManager": { + "model": "Model", + "manual": "Manual", + "name": "Nume", + "description": "Descriere", + "config": "Configurare", + "width": "Lățime", + "height": "Înălțime", + "search": "Caută", + "load": "Încarcă", + "active": "activ", + "selected": "Selectat", + "delete": "Șterge", + "convert": "Convertește", + "alpha": "Alpha", + "none": "niciunul", + "vae": "VAE", + "variant": "Variantă", + "settings": "Setări", + "advanced": "Avansat", + "cancel": "Anulează", + "edit": "Editează", + "path": "Path", + "prune": "Taie", + "source": "Sursă", + "metadata": "Metadata", + "huggingFace": "HuggingFace", + "huggingFacePlaceholder": "autor/nume-model", + "install": "Instalează", + "loraModels": "LoRAs", + "main": "Main" + }, + "parameters": { + "general": "General", + "images": "Imagini", + "steps": "Pași", + "width": "Lățime", + "height": "Înălțime", + "seed": "Seed", + "type": "Tip", + "strength": "Putere", + "upscaling": "Upscaling", + "scale": "Scale", + "symmetry": "Simetrie", + "info": "Informații", + "scheduler": "Planificator", + "coherenceMode": "Mod", + "patchmatchDownScaleSize": "Downscale", + "cancel": { + "cancel": "Anulează" + }, + "invoke": { + "invoke": "Invocă" + }, + "iterations": "Iterații", + "aspect": "Aspect" + }, + "settings": { + "models": "Modele", + "developer": "Developer", + "general": "General", + "generation": "Generare", + "beta": "Beta" + }, + "boards": { + "cancel": "Anulează", + "loading": "Se încarcă...", + "move": "Mută", + "uncategorized": "Necategorizat", + "archived": "Arhivat", + "boards": "Boards" + }, + "gallery": { + "copy": "Copiază", + "download": "Descarcă", + "loading": "Se încarcă", + "drop": "Lasă", + "image": "imagine", + "starImage": "Adaugă la favorite", + "unstarImage": "Elimină de la favorite", + "slider": "Slider", + "sideBySide": "Side-by-Side", + "hover": "Hover", + "go": "Du-te", + "gallery": "Galerie" + }, + "metadata": { + "height": "Înălțime", + "metadata": "Metadata", + "model": "Model", + "scheduler": "Planificator", + "seed": "Seed", + "steps": "Pași", + "width": "Lățime", + "workflow": "Workflow", + "vae": "VAE", + "cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)" + }, + "models": { + "loading": "se încarcă", + "lora": "LoRA", + "concepts": "Concepte" + }, + "nodes": { + "notes": "Note", + "workflow": "Workflow", + "workflowAuthor": "Autor", + "workflowContact": "Contact", + "workflowName": "Nume", + "workflowNotes": "Note", + "workflowTags": "Etichete", + "workflowVersion": "Versiune", + "executionStateError": "Eroare", + "executionStateCompleted": "Completat", + "version": "Versiune", + "boolean": "Booleani", + "collection": "Colecție", + "edge": "Muchie", + "enum": "Enum", + "float": "Float", + "integer": "Integer", + "node": "Nod", + "scheduler": "Planificator", + "string": "String", + "ipAdapter": "IP-Adapter", + "edit": "Editează", + "graph": "Graf" + }, + "sdxl": { + "loading": "Se încarcă...", + "refiner": "Refiner", + "scheduler": "Planificator", + "steps": "Pași" + }, + "queue": { + "queue": "Coadă", + "resume": "Reia", + "pause": "Întrerupe", + "cancel": "Anulează", + "prune": "Taie", + "clear": "Golește", + "current": "Curent", + "next": "Următorul", + "status": "Status", + "total": "Total", + "pending": "În așteptare", + "completed": "Completat", + "failed": "Eșuat", + "canceled": "Anulat", + "batch": "Lot", + "item": "Item", + "session": "Sesiune", + "front": "față", + "back": "spate", + "time": "Timp", + "origin": "Origine", + "destination": "Destinație", + "upscaling": "Upscaling", + "canvas": "Canvas", + "generation": "Generare", + "workflows": "Workflows", + "other": "Altele", + "gallery": "Galerie" + }, + "popovers": { + "compositingCoherenceMode": { + "heading": "Mod" + }, + "controlNetWeight": { + "heading": "Weight" + }, + "lora": { + "heading": "LoRA" + }, + "paramModel": { + "heading": "Model" + }, + "paramScheduler": { + "heading": "Planificator" + }, + "paramSeed": { + "heading": "Seed" + }, + "paramSteps": { + "heading": "Pași" + }, + "paramVAE": { + "heading": "VAE" + }, + "paramIterations": { + "heading": "Iterații" + }, + "controlNet": { + "heading": "ControlNet" + }, + "controlNetProcessor": { + "heading": "Procesator" + }, + "loraWeight": { + "heading": "Weight" + }, + "paramAspect": { + "heading": "Aspect" + }, + "paramHeight": { + "heading": "Înălțime" + }, + "paramWidth": { + "heading": "Lățime" + }, + "patchmatchDownScaleSize": { + "heading": "Downscale" + }, + "refinerScheduler": { + "heading": "Planificator" + }, + "refinerSteps": { + "heading": "Pași" + }, + "ipAdapterMethod": { + "heading": "Mod" + }, + "scale": { + "heading": "Scale" + }, + "creativity": { + "heading": "Creativitate" + }, + "structure": { + "heading": "Structură" + } + }, + "invocationCache": { + "clear": "Golește", + "enable": "Activează", + "disable": "Dezactivează" + }, + "workflows": { + "workflows": "Workflows", + "ascending": "În ordine crescătoare", + "created": "Creat", + "descending": "În ordine descrescătoare", + "opened": "Deschis", + "updated": "Actualizat", + "name": "Nume" + }, + "accordions": { + "generation": { + "title": "Generare" + }, + "image": { + "title": "Imagine" + }, + "advanced": { + "title": "Avansat" + }, + "control": { + "title": "Control" + }, + "compositing": { + "title": "Se compune", + "infillTab": "Infill" + } + }, + "toast": { + "parameters": "Parametri" + }, + "controlLayers": { + "rectangle": "Dreptunghi", + "opacity": "Opacitate", + "duplicate": "Duplică", + "width": "Lățime", + "transparency": "Transparență", + "locked": "Blocat", + "unlocked": "Deblocat", + "fill": { + "solid": "Solid", + "grid": "Grid", + "crosshatch": "Crosshatch", + "vertical": "Vertical", + "horizontal": "Orizontal", + "diagonal": "Diagonal" + }, + "tool": { + "brush": "Pensulă", + "eraser": "Radieră", + "rectangle": "Dreptunghi", + "bbox": "Bbox", + "move": "Mută", + "view": "Vizualizează" + }, + "filter": { + "filter": "Filtrează", + "filters": "Filtre", + "apply": "Aplică", + "cancel": "Anulează", + "reset": "Resetare", + "process": "Procesează", + "spandrel_filter": { + "model": "Model" + }, + "depth_anything_depth_estimation": { + "model_size_small": "Mică", + "model_size_base": "Bază", + "model_size_large": "Mare" + }, + "hed_edge_detection": { + "scribble": "Scribble" + }, + "lineart_edge_detection": { + "coarse": "Coarse" + }, + "pidi_edge_detection": { + "scribble": "Scribble" + } + }, + "transform": { + "transform": "Transformă", + "reset": "Resetează", + "apply": "Aplică", + "cancel": "Anulează" + }, + "settings": { + "snapToGrid": { + "off": "Oprit", + "on": "Pornit" + } + }, + "HUD": { + "bbox": "Bbox" + }, + "canvas": "Canvas", + "regional": "Regional", + "global": "Global", + "prompt": "Prompt", + "weight": "Weight" + }, + "ui": { + "tabs": { + "canvas": "Canvas", + "workflows": "Workflows", + "models": "Modele", + "queue": "Coadă", + "upscaling": "Upscaling", + "gallery": "Galerie" + } + }, + "upscaling": { + "creativity": "Creativitate", + "structure": "Structură", + "scale": "Scale", + "upscale": "Upscale" + }, + "stylePresets": { + "active": "Activ", + "name": "Nume", + "preview": "Previzualizare", + "private": "Privat", + "shared": "Partajat", + "type": "Tip", + "nameColumn": "'nume'", + "negativePromptColumn": "'negative_prompt'" + }, + "system": { + "logLevel": { + "trace": "Trace", + "debug": "Debug", + "info": "Informații", + "warn": "Avertizează", + "error": "Eroare", + "fatal": "Fatal" + }, + "logNamespaces": { + "gallery": "Galerie", + "models": "Modele", + "config": "Configurare", + "canvas": "Canvas", + "generation": "Generare", + "workflows": "Workflows", + "system": "Sistem", + "events": "Evenimente", + "queue": "Coadă", + "metadata": "Metadata" + } + } +} diff --git a/invokeai/frontend/web/public/locales/ru.json b/invokeai/frontend/web/public/locales/ru.json new file mode 100644 index 00000000000..279b1b0eabe --- /dev/null +++ b/invokeai/frontend/web/public/locales/ru.json @@ -0,0 +1,2115 @@ +{ + "common": { + "hotkeysLabel": "Горячие клавиши", + "languagePickerLabel": "Язык", + "reportBugLabel": "Сообщить об ошибке", + "settingsLabel": "Настройки", + "img2img": "Изображение в изображение (img2img)", + "nodes": "Схемы", + "upload": "Загрузить", + "load": "Загрузить", + "statusDisconnected": "Отключен", + "githubLabel": "Github", + "discordLabel": "Discord", + "loading": "Загрузка", + "back": "Назад", + "cancel": "Отменить", + "accept": "Принять", + "postprocessing": "Постобработка", + "txt2img": "Текст в изображение (txt2img)", + "linear": "Линейный вид", + "dontAskMeAgain": "Больше не спрашивать", + "areYouSure": "Вы уверены?", + "random": "Случайное", + "openInNewTab": "Открыть в новой вкладке", + "communityLabel": "Сообщество", + "batch": "Пакетный менеджер", + "modelManager": "Менеджер моделей", + "controlNet": "ControlNet", + "advanced": "Расширенные", + "t2iAdapter": "T2I адаптер", + "checkpoint": "Checkpoint", + "format": "Формат", + "unknown": "Неизвестно", + "folder": "Папка", + "inpaint": "Перерисовать", + "updated": "Обновлен", + "on": "Вкл", + "save": "Сохранить", + "created": "Создано", + "error": "Ошибка", + "simple": "Простой", + "ipAdapter": "IP адаптер", + "installed": "Установлено", + "ai": "ИИ", + "auto": "Авто", + "file": "Файл", + "delete": "Удалить", + "template": "Шаблон", + "outputs": "результаты", + "unknownError": "Неизвестная ошибка", + "direction": "Направление", + "data": "Данные", + "somethingWentWrong": "Что-то пошло не так", + "safetensors": "Safetensors", + "outpaint": "Расширить изображение", + "orderBy": "Сортировать по", + "copyError": "Ошибка $t(gallery.copy)", + "learnMore": "Узнать больше", + "saveAs": "Сохранить как", + "input": "Вход", + "details": "Детали", + "or": "или", + "aboutHeading": "Управляй своей творческой силой", + "red": "Красный", + "green": "Зеленый", + "blue": "Синий", + "alpha": "Альфа", + "toResolve": "Чтоб решить", + "copy": "Копировать", + "aboutDesc": "Используете Invoke в работе? Ознакомьтесь:", + "add": "Добавить", + "beta": "Бета", + "selected": "Выбрано", + "positivePrompt": "Позитивный промпт", + "negativePrompt": "Негативный промпт", + "editor": "Редактор", + "tab": "Вкладка", + "enabled": "Включено", + "disabled": "Отключено", + "dontShowMeThese": "Больше не показывать", + "apply": "Применить", + "loadingImage": "Загрузка изображения", + "off": "Выкл", + "openInViewer": "Открыть в просмотрщике", + "edit": "Редактировать", + "view": "Просмотреть", + "placeholderSelectAModel": "Выбрать модель", + "reset": "Сброс", + "none": "Ничего", + "new": "Новый", + "ok": "Ok", + "close": "Закрыть", + "error_withCount_one": "{{count}} Ошибка", + "error_withCount_few": "{{count}} Ошибки", + "error_withCount_many": "{{count}} Ошибок", + "model_withCount_one": "{{count}} Модель", + "model_withCount_few": "{{count}} Модели", + "model_withCount_many": "{{count}} Моделей", + "options_withCount_one": "{{count}} Опция", + "options_withCount_few": "{{count}} Опции", + "options_withCount_many": "{{count}} Опций", + "crop": "Обрезать" + }, + "gallery": { + "galleryImageSize": "Размер изображений", + "gallerySettings": "Настройка галереи", + "autoSwitchNewImages": "Автоматически выбирать новые", + "deleteImagePermanent": "Удаленные изображения невозможно восстановить.", + "deleteImage_one": "Удалить изображение", + "deleteImage_few": "Удалить {{count}} изображения", + "deleteImage_many": "Удалить {{count}} изображений", + "autoAssignBoardOnClick": "Авто-назначение доски по клику", + "deleteSelection": "Удалить выделенное", + "featuresWillReset": "Если вы удалите это изображение, эти функции будут немедленно сброшены.", + "loading": "Загрузка", + "image": "изображение", + "drop": "перебросить", + "downloadSelection": "Скачать выделенное", + "currentlyInUse": "В настоящее время это изображение используется в следующих функциях:", + "unstarImage": "Удалить из избранного", + "dropOrUpload": "Перетащите или загрузите", + "copy": "Копировать", + "download": "Скачать", + "noImageSelected": "Изображение не выбрано", + "starImage": "Добавить в избранное", + "dropToUpload": "$t(gallery.drop) чтоб загрузить", + "bulkDownloadFailed": "Загрузка не удалась", + "bulkDownloadRequested": "Подготовка к скачиванию", + "bulkDownloadRequestedDesc": "Ваш запрос на скачивание готовится. Это может занять несколько минут.", + "bulkDownloadRequestFailed": "Возникла проблема при подготовке скачивания", + "alwaysShowImageSizeBadge": "Всегда показывать значок размера изображения", + "openInViewer": "Открыть в просмотрщике", + "selectForCompare": "Выбрать для сравнения", + "hover": "Наведение", + "swapImages": "Поменять местами", + "stretchToFit": "Растягивание до нужного размера", + "exitCompare": "Выйти из сравнения", + "compareHelp4": "Нажмите Z или Esc для выхода.", + "compareImage": "Сравнить изображение", + "viewerImage": "Изображение просмотрщика", + "slider": "Слайдер", + "sideBySide": "Бок о бок", + "compareHelp1": "Удерживайте Alt при нажатии на изображение в галерее или при помощи клавиш со стрелками, чтобы изменить сравниваемое изображение.", + "compareHelp2": "Нажмите M, чтобы переключиться между режимами сравнения.", + "compareHelp3": "Нажмите C, чтобы поменять местами сравниваемые изображения.", + "newestFirst": "Сначала новые", + "sortDirection": "Направление сортировки", + "oldestFirst": "Сначала старые", + "showStarredImagesFirst": "Сначала избранные изображения", + "selectAllOnPage": "Выбрать все на странице", + "showArchivedBoards": "Показать архивированные доски", + "searchImages": "Поиск по метаданным", + "displayBoardSearch": "Поиск доски", + "displaySearch": "Поиск изображений", + "exitBoardSearch": "Выйти из поиска досок", + "go": "Перейти", + "exitSearch": "Выйти из поиска изображений", + "move": "Двигать", + "gallery": "Галерея", + "imagesTab": "Изображения, созданные и сохраненные в Invoke.", + "assetsTab": "Файлы, которые вы загрузили для использования в своих проектах.", + "boardsSettings": "Настройки доски", + "imagesSettings": "Настройки галереи изображений" + }, + "hotkeys": { + "searchHotkeys": "Поиск горячих клавиш", + "noHotkeysFound": "Горячие клавиши не найдены", + "clearSearch": "Очистить поиск", + "app": { + "title": "Приложение", + "invoke": { + "desc": "Добавить генерацию в конец очереди.", + "title": "Сгенерировать" + }, + "clearQueue": { + "title": "Очистить очередь", + "desc": "Отмена и очистка всех элементов очереди." + }, + "selectCanvasTab": { + "title": "Выбрать вкладку Холст", + "desc": "Выбирает вкладку Холст." + }, + "selectUpscalingTab": { + "title": "Выбрать вкладку Увеличение", + "desc": "Выбирает вкладку увеличения." + }, + "selectWorkflowsTab": { + "title": "Выбрать вкладку Рабочие Процессы", + "desc": "Выбирает вкладку рабочих процессов." + }, + "focusPrompt": { + "title": "Сфокусироваться на запросе", + "desc": "Перемещает фокус курсора на положительный запрос." + }, + "toggleLeftPanel": { + "title": "Переключить левую панель", + "desc": "Показывает или скрывает левую панель." + }, + "resetPanelLayout": { + "desc": "Верните левую и правую панели к размерам и расположению по умолчанию.", + "title": "Сброс расположения панелей" + }, + "invokeFront": { + "title": "Сгенерировать (вперед)", + "desc": "Добавьте генерацию вперед очереди." + }, + "cancelQueueItem": { + "title": "Отмена", + "desc": "Отмена текущего обрабатываемого элемента очереди." + }, + "selectModelsTab": { + "desc": "Выбирает вкладку моделей.", + "title": "Выбрать вкладку Модели" + }, + "selectQueueTab": { + "title": "Выбрать вкладку Очередь", + "desc": "Выбирает вкладку очереди." + }, + "togglePanels": { + "title": "Переключить панели", + "desc": "Показать или скрыть одновременно левую и правую панели." + }, + "toggleRightPanel": { + "title": "Переключить правую панель", + "desc": "Показывает или скрывает правую панель." + } + }, + "canvas": { + "title": "Холст", + "selectBrushTool": { + "title": "Инструмент кисть", + "desc": "Выбирает кисть." + }, + "selectBboxTool": { + "title": "Инструмент рамка", + "desc": "Выбрать инструмент «Ограничительная рамка»." + }, + "incrementToolWidth": { + "desc": "Increment the brush or eraser tool width, whichever is selected.", + "title": "Increment Tool Width" + }, + "selectColorPickerTool": { + "title": "Color Picker Tool", + "desc": "Select the color picker tool." + }, + "prevEntity": { + "title": "Prev Layer", + "desc": "Select the previous layer in the list." + }, + "filterSelected": { + "title": "Filter", + "desc": "Применяет фильтр к выбранному слою. Применимо только к растровым слоям и слоям управления." + }, + "undo": { + "desc": "Отменяет последнее действие на холсте.", + "title": "Отменить" + }, + "transformSelected": { + "title": "Transform", + "desc": "Transform the selected layer." + }, + "setZoomTo400Percent": { + "title": "Zoom to 400%", + "desc": "Set the canvas zoom to 400%." + }, + "setZoomTo200Percent": { + "title": "Zoom to 200%", + "desc": "Set the canvas zoom to 200%." + }, + "deleteSelected": { + "desc": "Delete the selected layer.", + "title": "Delete Layer" + }, + "resetSelected": { + "title": "Reset Layer", + "desc": "Reset the selected layer. Only applies to Inpaint Mask and Regional Guidance." + }, + "redo": { + "desc": "Возвращает последнее отмененное действие.", + "title": "Вернуть" + }, + "nextEntity": { + "title": "Next Layer", + "desc": "Select the next layer in the list." + }, + "applyFilter": { + "title": "Apply Filter", + "desc": "Apply the pending filter to the selected layer." + }, + "cancelFilter": { + "title": "Cancel Filter", + "desc": "Cancel the pending filter." + }, + "applyTransform": { + "desc": "Apply the pending transform to the selected layer.", + "title": "Apply Transform" + }, + "cancelTransform": { + "title": "Cancel Transform", + "desc": "Cancel the pending transform." + }, + "selectEraserTool": { + "title": "Eraser Tool", + "desc": "Select the eraser tool." + }, + "fitLayersToCanvas": { + "desc": "Scale and position the view to fit all visible layers.", + "title": "Fit Layers to Canvas" + }, + "decrementToolWidth": { + "title": "Decrement Tool Width", + "desc": "Decrement the brush or eraser tool width, whichever is selected." + }, + "setZoomTo800Percent": { + "title": "Zoom to 800%", + "desc": "Set the canvas zoom to 800%." + }, + "quickSwitch": { + "title": "Layer Quick Switch", + "desc": "Switch between the last two selected layers. If a layer is bookmarked, always switch between it and the last non-bookmarked layer." + }, + "fitBboxToCanvas": { + "title": "Fit Bbox to Canvas", + "desc": "Scale and position the view to fit the bbox." + }, + "setZoomTo100Percent": { + "title": "Zoom to 100%", + "desc": "Set the canvas zoom to 100%." + }, + "selectMoveTool": { + "desc": "Select the move tool.", + "title": "Move Tool" + }, + "selectRectTool": { + "title": "Rect Tool", + "desc": "Select the rect tool." + }, + "selectViewTool": { + "title": "View Tool", + "desc": "Select the view tool." + } + }, + "hotkeys": "Горячие клавиши", + "workflows": { + "undo": { + "title": "Отмена", + "desc": "Отменить последнее действие в рабочем процессе." + }, + "deleteSelection": { + "desc": "Удалить выделенные узлы и ребра.", + "title": "Delete" + }, + "redo": { + "title": "Вернуть", + "desc": "Вернуть последнее действие в рабочем процессе." + }, + "copySelection": { + "title": "Copy", + "desc": "Copy selected nodes and edges." + }, + "pasteSelection": { + "title": "Paste", + "desc": "Paste copied nodes and edges." + }, + "addNode": { + "desc": "Open the add node menu.", + "title": "Add Node" + }, + "title": "Workflows", + "pasteSelectionWithEdges": { + "title": "Paste with Edges", + "desc": "Paste copied nodes, edges, and all edges connected to copied nodes." + }, + "selectAll": { + "desc": "Select all nodes and edges.", + "title": "Select All" + } + }, + "viewer": { + "nextComparisonMode": { + "title": "Следующий режим сравнения", + "desc": "Циклическое переключение режимов сравнения." + }, + "loadWorkflow": { + "desc": "Загрузить сохраненный рабочий процесс текущего изображения (если он есть).", + "title": "Загрузить рабочий процесс" + }, + "recallAll": { + "desc": "Восстановить все метаданные текущего изображения.", + "title": "Восстановить все метаданные" + }, + "swapImages": { + "desc": "Поменять местами сравниваемые изображения.", + "title": "Swap Comparison Images" + }, + "title": "Просмотрщик изображений", + "toggleViewer": { + "title": "Открыть/закрыть просмотрщик", + "desc": "Показать или скрыть просмотрщик изображений. Доступно только на вкладке «Холст»." + }, + "recallSeed": { + "title": "Recall Seed", + "desc": "Recall the seed for the current image." + }, + "recallPrompts": { + "desc": "Recall the positive and negative prompts for the current image.", + "title": "Recall Prompts" + }, + "remix": { + "title": "Remix", + "desc": "Recall all metadata except for the seed for the current image." + }, + "useSize": { + "desc": "Use the current image's size as the bbox size.", + "title": "Use Size" + }, + "runPostprocessing": { + "title": "Run Postprocessing", + "desc": "Run the selected postprocessing on the current image." + }, + "toggleMetadata": { + "title": "Show/Hide Metadata", + "desc": "Show or hide the current image's metadata overlay." + } + }, + "gallery": { + "galleryNavRightAlt": { + "desc": "Same as Navigate Right, but selects the compare image, opening compare mode if it isn't already open.", + "title": "Navigate Right (Compare Image)" + }, + "galleryNavRight": { + "desc": "Navigate right in the gallery grid, selecting that image. If at the last image of the row, go to the next row. If at the last image of the page, go to the next page.", + "title": "Navigate Right" + }, + "galleryNavUp": { + "desc": "Navigate up in the gallery grid, selecting that image. If at the top of the page, go to the previous page.", + "title": "Navigate Up" + }, + "galleryNavDown": { + "title": "Navigate Down", + "desc": "Navigate down in the gallery grid, selecting that image. If at the bottom of the page, go to the next page." + }, + "galleryNavLeft": { + "title": "Navigate Left", + "desc": "Navigate left in the gallery grid, selecting that image. If at the first image of the row, go to the previous row. If at the first image of the page, go to the previous page." + }, + "galleryNavDownAlt": { + "title": "Navigate Down (Compare Image)", + "desc": "Same as Navigate Down, but selects the compare image, opening compare mode if it isn't already open." + }, + "galleryNavLeftAlt": { + "desc": "Same as Navigate Left, but selects the compare image, opening compare mode if it isn't already open.", + "title": "Navigate Left (Compare Image)" + }, + "clearSelection": { + "desc": "Clear the current selection, if any.", + "title": "Clear Selection" + }, + "deleteSelection": { + "title": "Delete", + "desc": "Delete all selected images. By default, you will be prompted to confirm deletion. If the images are currently in use in the app, you will be warned." + }, + "galleryNavUpAlt": { + "title": "Navigate Up (Compare Image)", + "desc": "Same as Navigate Up, but selects the compare image, opening compare mode if it isn't already open." + }, + "title": "Gallery", + "selectAllOnPage": { + "title": "Select All On Page", + "desc": "Select all images on the current page." + } + } + }, + "modelManager": { + "modelManager": "Менеджер моделей", + "model": "Модель", + "modelUpdated": "Модель обновлена", + "manual": "Ручное", + "name": "Название", + "description": "Описание", + "config": "Файл конфигурации", + "width": "Ширина", + "height": "Высота", + "addModel": "Добавить модель", + "availableModels": "Доступные модели", + "search": "Искать", + "load": "Загрузить", + "active": "активна", + "selected": "Выбраны", + "delete": "Удалить", + "deleteModel": "Удалить модель", + "deleteConfig": "Удалить конфигурацию", + "deleteMsg1": "Вы точно хотите удалить модель из InvokeAI?", + "deleteMsg2": "Это приведет К УДАЛЕНИЮ модели С ДИСКА, если она находится в корневой папке Invoke. Если вы используете пользовательское расположение, то модель НЕ будет удалена с диска.", + "convertToDiffusersHelpText5": "Пожалуйста, убедитесь, что у вас достаточно места на диске. Модели обычно занимают 2–7 Гб.", + "convertToDiffusersHelpText3": "Файл чекпоинта будет удалён с диска, если он находится в корневой папке InvokeAI. Если файл расположен в пользовательской папке, он удалён не будет.", + "allModels": "Все модели", + "repo_id": "ID репозитория", + "convert": "Преобразовать", + "convertToDiffusers": "Преобразовать в Diffusers", + "convertToDiffusersHelpText1": "Модель будет преобразована в формат 🧨 Diffusers.", + "convertToDiffusersHelpText4": "Это единоразовое действие. Оно может занять 30—60 секунд в зависимости от характеристик вашего компьютера.", + "convertToDiffusersHelpText6": "Вы хотите преобразовать эту модель?", + "modelConverted": "Модель преобразована", + "alpha": "Альфа", + "none": "пусто", + "convertToDiffusersHelpText2": "Этот процесс заменит вашу запись в менеджере моделей на версию той же модели в Diffusers.", + "modelDeleted": "Модель удалена", + "variant": "Вариант", + "baseModel": "Базовая модель", + "vae": "VAE", + "modelDeleteFailed": "Не удалось удалить модель", + "convertingModelBegin": "Конвертация модели. Пожалуйста, подождите.", + "settings": "Настройки", + "selectModel": "Выберите модель", + "syncModels": "Синхронизация моделей", + "modelUpdateFailed": "Не удалось обновить модель", + "modelConversionFailed": "Не удалось сконвертировать модель", + "predictionType": "Тип прогноза", + "advanced": "Продвинутый", + "modelType": "Тип модели", + "vaePrecision": "Точность VAE", + "noModelSelected": "Модель не выбрана", + "addModels": "Добавить модели", + "cancel": "Отмена", + "defaultSettings": "Стандартные настройки", + "metadata": "Метаданные", + "imageEncoderModelId": "ID модели-энкодера изображений", + "typePhraseHere": "Введите фразы здесь", + "defaultSettingsSaved": "Стандартные настройки сохранены", + "edit": "Редактировать", + "path": "Путь", + "prune": "Удалить", + "pruneTooltip": "Удалить готовые импорты из очереди", + "repoVariant": "Вариант репозитория", + "scanFolder": "Сканировать папку", + "scanResults": "Результаты сканирования", + "source": "Источник", + "triggerPhrases": "Триггерные фразы", + "modelName": "Название модели", + "modelSettings": "Настройки модели", + "upcastAttention": "Внимание", + "deleteModelImage": "Удалить изображение модели", + "uploadImage": "Загрузить изображение", + "inplaceInstall": "Установка на месте", + "localOnly": "только локально", + "modelImageDeleted": "Изображение модели удалено", + "modelImageDeleteFailed": "Не получилось удалить изображение модели", + "modelImageUpdated": "Изображение модели обновлено", + "modelImageUpdateFailed": "Не удалось обновить изображение модели", + "pathToConfig": "Путь к конфигурации", + "loraTriggerPhrases": "Триггерные фразы LoRA", + "mainModelTriggerPhrases": "Триггерные фразы основной модели", + "inplaceInstallDesc": "Устанавливать модели без перемещения файлов. В этом случае модель будет загружаться из исходной папки. Если опция отключена, файлы модели при установке будут перемещены в каталог моделей Invoke.", + "huggingFaceRepoID": "ID репозитория HuggingFace", + "installQueue": "Очередь установки", + "installAll": "Установить все", + "install": "Установить", + "huggingFace": "HuggingFace", + "huggingFacePlaceholder": "пользователь/имя-модели", + "huggingFaceHelper": "Если в этом репозитории найдено несколько моделей, вам будет предложено выбрать одну из них для установки.", + "installRepo": "Установить репозиторий", + "scanFolderHelper": "Папка будет рекурсивно просканирована на наличие моделей. Для очень больших папок это может занять несколько минут.", + "scanPlaceholder": "Путь к локальной папке", + "simpleModelPlaceholder": "URL или путь к локальному файлу или папке diffusers", + "urlOrLocalPath": "URL или локальный путь", + "urlOrLocalPathHelper": "URL-адреса должны указывать на один файл. Локальные пути могут указывать на один файл или папку для одной модели диффузоров.", + "starterModels": "Стартовые модели", + "textualInversions": "Текстовые инверсии", + "loraModels": "LoRAs", + "main": "Основные", + "noModelsInstalled": "Нет установленных моделей", + "noModelsInstalledDesc1": "Установите модели с помощью", + "noMatchingModels": "Нет подходящих моделей", + "learnMoreAboutSupportedModels": "Подробнее о поддерживаемых моделях", + "t5Encoder": "T5 энкодер", + "spandrelImageToImage": "Image to Image (Spandrel)", + "clipEmbed": "CLIP Embed", + "installingXModels_one": "Установка {{count}} модели", + "installingXModels_few": "Установка {{count}} моделей", + "installingXModels_many": "Установка {{count}} моделей", + "installingBundle": "Установка пакета", + "installingModel": "Установка модели", + "starterBundles": "Стартовые пакеты", + "skippingXDuplicates_one": ", пропуская {{count}} дубликат", + "skippingXDuplicates_few": ", пропуская {{count}} дубликата", + "skippingXDuplicates_many": ", пропуская {{count}} дубликатов", + "includesNModels": "Включает в себя {{n}} моделей и их зависимостей.", + "starterBundleHelpText": "Легко установите все модели, необходимые для начала работы с базовой моделью, включая основную модель, ControlNet, IP-адаптеры и другие. При выборе набора уже установленные модели будут пропущены." + }, + "parameters": { + "images": "Изображения", + "steps": "Шаги", + "cfgScale": "Шкала точности (CFG)", + "width": "Ширина", + "height": "Высота", + "seed": "Сид", + "shuffle": "Обновить сид", + "noiseThreshold": "Порог шума", + "perlinNoise": "Шум Перлина", + "type": "Тип", + "strength": "Сила", + "upscaling": "Увеличение", + "scale": "Масштаб", + "imageFit": "Уместить изображение", + "scaleBeforeProcessing": "Масштабировать", + "scaledWidth": "Масштаб Ш", + "scaledHeight": "Масштаб В", + "infillMethod": "Способ заполнения", + "tileSize": "Размер области", + "usePrompt": "Использовать запрос", + "useSeed": "Использовать сид", + "useAll": "Использовать все", + "info": "Метаданные", + "cancel": { + "cancel": "Отмена" + }, + "general": "Основное", + "symmetry": "Симметрия", + "denoisingStrength": "Сила зашумления", + "copyImage": "Скопировать изображение", + "seamlessXAxis": "Бесшовная ось X", + "seamlessYAxis": "Бесшовная ось Y", + "scheduler": "Планировщик", + "positivePromptPlaceholder": "Запрос", + "negativePromptPlaceholder": "Исключающий запрос", + "controlNetControlMode": "Режим управления", + "clipSkip": "CLIP Пропуск", + "maskBlur": "Размытие маски", + "invoke": { + "noNodesInGraph": "Нет узлов в графе", + "noModelSelected": "Модель не выбрана", + "noPrompts": "Подсказки не создаются", + "missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} отсутствует ввод", + "systemDisconnected": "Система отключена", + "missingNodeTemplate": "Отсутствует шаблон узла", + "missingFieldTemplate": "Отсутствует шаблон поля", + "addingImagesTo": "Добавление изображений в", + "invoke": "Создать", + "noFLUXVAEModelSelected": "Для генерации FLUX не выбрана модель VAE", + "noT5EncoderModelSelected": "Для генерации FLUX не выбрана модель T5 энкодера", + "canvasIsFiltering": "Холст фильтруется", + "canvasIsTransforming": "Холст трансформируется", + "noCLIPEmbedModelSelected": "Для генерации FLUX не выбрана модель CLIP Embed", + "canvasIsRasterizing": "Холст занят (идёт растеризация)", + "canvasIsCompositing": "Холст занят (идёт компоновка)" + }, + "cfgRescaleMultiplier": "Множитель масштабирования CFG", + "patchmatchDownScaleSize": "уменьшить", + "useCpuNoise": "Использовать шум CPU", + "imageActions": "Действия с изображениями", + "iterations": "Кол-во", + "useSize": "Использовать размер", + "coherenceMode": "Режим", + "aspect": "Соотношение", + "swapDimensions": "Поменять местами", + "setToOptimalSize": "Установить оптимальный для модели размер", + "setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (может быть слишком маленьким)", + "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (может быть слишком большим)", + "lockAspectRatio": "Заблокировать соотношение", + "remixImage": "Ремикс изображения", + "coherenceMinDenoise": "Мин. шумоподавление", + "coherenceEdgeSize": "Размер края", + "infillColorValue": "Цвет заливки", + "postProcessing": "Постобработка (Shift + U)", + "processImage": "Обработка изображения", + "sendToUpscale": "Отправить на увеличение", + "gaussianBlur": "Размытие по Гауссу", + "staged": "Инсценировка", + "optimizedImageToImage": "Оптимизированное img2img", + "sendToCanvas": "Отправить на холст", + "guidance": "Точность", + "boxBlur": "Box Blur", + "images_withCount_one": "Изображение", + "images_withCount_few": "Изображения", + "images_withCount_many": "Изображений" + }, + "settings": { + "models": "Модели", + "displayInProgress": "Показывать процесс генерации", + "confirmOnDelete": "Подтверждать удаление", + "resetWebUI": "Сброс настроек веб-интерфейса", + "resetWebUIDesc1": "Сброс настроек веб-интерфейса удаляет только локальный кэш браузера с вашими изображениями и настройками. Он не удаляет изображения с диска.", + "resetWebUIDesc2": "Если изображения не отображаются в галерее или не работает что-то еще, пожалуйста, попробуйте сбросить настройки, прежде чем сообщать о проблеме на GitHub.", + "resetComplete": "Настройки веб-интерфейса были сброшены.", + "developer": "Разработчик", + "general": "Основное", + "showProgressInViewer": "Показывать процесс генерации в Просмотрщике", + "antialiasProgressImages": "Сглаживать предпоказ процесса генерации", + "generation": "Поколение", + "ui": "Пользовательский интерфейс", + "beta": "Бета", + "clearIntermediates": "Очистить промежуточные", + "clearIntermediatesDesc3": "Изображения вашей галереи не будут удалены.", + "clearIntermediatesWithCount_one": "Очистить {{count}} промежуточное", + "clearIntermediatesWithCount_few": "Очистить {{count}} промежуточных", + "clearIntermediatesWithCount_many": "Очистить {{count}} промежуточных", + "enableNSFWChecker": "Включить NSFW проверку", + "clearIntermediatesDisabled": "Очередь должна быть пуста, чтобы очистить промежуточные продукты", + "clearIntermediatesDesc2": "Промежуточные изображения — это побочные продукты генерации, отличные от результирующих изображений в галерее. Очистка промежуточных файлов освободит место на диске.", + "enableInvisibleWatermark": "Включить невидимый водяной знак", + "enableInformationalPopovers": "Включить информационные всплывающие окна", + "intermediatesCleared_one": "Очищено {{count}} промежуточное", + "intermediatesCleared_few": "Очищено {{count}} промежуточных", + "intermediatesCleared_many": "Очищено {{count}} промежуточных", + "clearIntermediatesDesc1": "Очистка промежуточных данных приведёт к сбросу состояния холста и ControlNet.", + "intermediatesClearedFailed": "Проблема очистки промежуточных", + "reloadingIn": "Перезагрузка через", + "informationalPopoversDisabled": "Информационные всплывающие окна отключены", + "informationalPopoversDisabledDesc": "Информационные всплывающие окна были отключены. Включите их в Настройках.", + "confirmOnNewSession": "Подтверждение нового сеанса" + }, + "toast": { + "uploadFailed": "Загрузка не удалась", + "imageCopied": "Изображение скопировано", + "parametersNotSet": "Параметры не заданы", + "serverError": "Ошибка сервера", + "connected": "Подключено к серверу", + "canceled": "Обработка отменена", + "uploadFailedInvalidUploadDesc": "Допускаются только изображения в формате PNG, JPEG или WEBP.", + "parameterNotSet": "Параметр не задан", + "parameterSet": "Параметр задан", + "problemCopyingImage": "Не удается скопировать изображение", + "baseModelChangedCleared_one": "Очищена или отключена {{count}} несовместимая подмодель", + "baseModelChangedCleared_few": "Очищено или отключено {{count}} несовместимых подмодели", + "baseModelChangedCleared_many": "Очищено или отключено {{count}} несовместимых подмоделей", + "loadedWithWarnings": "Рабочий процесс загружен с предупреждениями", + "imageUploaded": "Изображение загружено", + "addedToBoard": "Добавлено в активы доски {{name}}", + "workflowLoaded": "Рабочий процесс загружен", + "problemDeletingWorkflow": "Проблема с удалением рабочего процесса", + "modelAddedSimple": "Модель добавлена в очередь", + "workflowDeleted": "Рабочий процесс удален", + "problemRetrievingWorkflow": "Проблема с получением рабочего процесса", + "imageUploadFailed": "Не удалось загрузить изображение", + "problemDownloadingImage": "Не удается скачать изображение", + "prunedQueue": "Урезанная очередь", + "modelImportCanceled": "Импорт модели отменен", + "parameters": "Параметры", + "parameterSetDesc": "Задан {{parameter}}", + "parameterNotSetDesc": "Невозможно задать {{parameter}}", + "baseModelChanged": "Базовая модель сменена", + "parameterNotSetDescWithMessage": "Не удалось задать {{parameter}}: {{message}}", + "parametersSet": "Параметры заданы", + "errorCopied": "Ошибка скопирована", + "sessionRef": "Сессия: {{sessionId}}", + "outOfMemoryError": "Ошибка нехватки памяти", + "outOfMemoryErrorDesc": "Ваши текущие настройки генерации превышают возможности системы. Пожалуйста, измените настройки и повторите попытку.", + "somethingWentWrong": "Что-то пошло не так", + "importFailed": "Импорт неудачен", + "importSuccessful": "Импорт успешен", + "problemSavingLayer": "Не удалось сохранить слой", + "sentToCanvas": "Отправить на холст", + "unableToLoadImage": "Невозможно загрузить изображение", + "unableToLoadImageMetadata": "Невозможно загрузить метаданные изображения", + "stylePresetLoaded": "Предустановка стиля загружена", + "problemCopyingLayer": "Не удалось скопировать слой", + "unableToLoadStylePreset": "Невозможно загрузить предустановку стиля", + "layerCopiedToClipboard": "Слой скопирован в буфер обмена", + "sentToUpscale": "Отправить на увеличение", + "linkCopied": "Ссылка скопирована", + "addedToUncategorized": "Добавлено в активы доски $t(boards.uncategorized)", + "imagesWillBeAddedTo": "Загруженные изображения будут добавлены в активы доски {{boardName}}.", + "schedulerResetZImageBase": "Планировщик LCM несовместим с моделями Z-Image Base. Переключено на Euler.", + "schedulerReset": "Планировщик сброшен", + "uploadFailedInvalidUploadDesc_withCount_one": "Допускается не более 1 изображения в формате PNG, JPEG или WEBP.", + "uploadFailedInvalidUploadDesc_withCount_few": "Допускается не более {{count}} изображения в формате PNG, JPEG или WEBP.", + "uploadFailedInvalidUploadDesc_withCount_many": "Допускается не более {{count}} изображений в формате PNG, JPEG или WEBP." + }, + "accessibility": { + "uploadImage": "Загрузить изображение", + "nextImage": "Следующее изображение", + "previousImage": "Предыдущее изображение", + "invokeProgressBar": "Индикатор выполнения", + "reset": "Сброс", + "menu": "Меню", + "mode": "Режим", + "resetUI": "$t(accessibility.reset) интерфейс", + "createIssue": "Сообщить о проблеме", + "about": "О программе", + "submitSupportTicket": "Отправить тикет в службу поддержки", + "toggleRightPanel": "Показать / скрыть правую панель (G)", + "toggleLeftPanel": "Показать / скрыть левую панель (T)", + "uploadImages": "Загрузить изображения" + }, + "nodes": { + "zoomInNodes": "Увеличьте масштаб", + "zoomOutNodes": "Уменьшите масштаб", + "fitViewportNodes": "Уместить вид", + "hideMinimapnodes": "Скрыть миникарту", + "showMinimapnodes": "Показать миникарту", + "loadWorkflow": "Загрузить рабочий процесс", + "reloadNodeTemplates": "Перезагрузить шаблоны узлов", + "downloadWorkflow": "Скачать JSON рабочего процесса", + "addNode": "Добавить узел", + "animatedEdges": "Анимированные ребра", + "animatedEdgesHelp": "Анимация выбранных ребер и ребер, соединенных с выбранными узлами", + "boolean": "Логические значения", + "cannotConnectInputToInput": "Невозможно подключить вход к входу", + "cannotConnectOutputToOutput": "Невозможно подключить выход к выходу", + "addNodeToolTip": "Добавить узел (Shift+A, Пробел)", + "scheduler": "Планировщик", + "missingTemplate": "Недопустимый узел: узел {{node}} типа {{type}} не имеет шаблона (не установлен?)", + "workflowDescription": "Краткое описание", + "inputFieldTypeParseError": "Невозможно разобрать тип поля ввода {{node}}.{{field}} ({{message}})", + "unsupportedAnyOfLength": "слишком много элементов объединения ({{count}})", + "unsupportedArrayItemType": "неподдерживаемый тип элемента массива \"{{type}}\"", + "noNodeSelected": "Узел не выбран", + "unableToValidateWorkflow": "Невозможно проверить рабочий процесс", + "enum": "Перечисления", + "updateAllNodes": "Обновить узлы", + "noOutputRecorded": "Выходы не зарегистрированы", + "updateApp": "Обновить приложение", + "colorCodeEdgesHelp": "Цветовая маркировка ребер в соответствии с их связанными полями", + "float": "Float", + "workflowContact": "Контакт", + "targetNodeFieldDoesNotExist": "Неверный край: целевое/вводное поле {{node}}.{{field}} не существует", + "unsupportedMismatchedUnion": "несовпадение типа CollectionOrScalar с базовыми типами {{firstType}} и {{secondType}}", + "allNodesUpdated": "Все узлы обновлены", + "integer": "Целое число", + "nodeTemplate": "Шаблон узла", + "nodeOpacity": "Непрозрачность узла", + "sourceNodeDoesNotExist": "Недопустимое ребро: исходный/выходной узел {{node}} не существует", + "unableToExtractEnumOptions": "невозможно извлечь параметры перечисления", + "snapToGrid": "Привязка к сетке", + "unableToParseFieldType": "невозможно проанализировать тип поля", + "nodeSearch": "Поиск узлов", + "updateNode": "Обновить узел", + "version": "Версия", + "validateConnections": "Проверка соединений и графика", + "inputMayOnlyHaveOneConnection": "Вход может иметь только одно соединение", + "notes": "Заметки", + "outputFieldTypeParseError": "Невозможно разобрать тип поля вывода {{node}}.{{field}} ({{message}})", + "nodeOutputs": "Выходы узла", + "currentImageDescription": "Отображает текущее изображение в редакторе узлов", + "validateConnectionsHelp": "Предотвратить создание недопустимых соединений и вызов недопустимых графиков", + "problemSettingTitle": "Проблема с настройкой названия", + "ipAdapter": "IP-адаптер", + "noConnectionInProgress": "Соединение не выполняется", + "workflowVersion": "Версия", + "fieldTypesMustMatch": "Типы полей должны совпадать", + "workflow": "Рабочий процесс", + "edge": "Край", + "sourceNodeFieldDoesNotExist": "Неверный край: поле источника/вывода {{node}}.{{field}} не существует", + "cannotDuplicateConnection": "Невозможно создать дубликаты соединений", + "noWorkflow": "Нет рабочего процесса", + "workflowTags": "Теги", + "fullyContainNodesHelp": "Чтобы узлы были выбраны, они должны полностью находиться в поле выбора", + "unableToGetWorkflowVersion": "Не удалось получить версию схемы рабочего процесса", + "workflowValidation": "Ошибка проверки рабочего процесса", + "nodePack": "Пакет узлов", + "nodeType": "Тип узла", + "fullyContainNodes": "Выбор узлов с полным содержанием", + "executionStateInProgress": "В процессе", + "unableToExtractSchemaNameFromRef": "невозможно извлечь имя схемы из ссылки", + "executionStateError": "Ошибка", + "prototypeDesc": "Этот вызов является прототипом. Он может претерпевать изменения при обновлении приложения и может быть удален в любой момент.", + "executionStateCompleted": "Выполнено", + "node": "Узел", + "workflowAuthor": "Автор", + "currentImage": "Текущее изображение", + "workflowName": "Название", + "collection": "Коллекция", + "unknownErrorValidatingWorkflow": "Неизвестная ошибка при проверке рабочего процесса", + "collectionFieldType": "{{name}} (Коллекция)", + "workflowNotes": "Примечания", + "string": "Строка", + "unknownNodeType": "Неизвестный тип узла", + "unableToUpdateNodes_one": "Невозможно обновить {{count}} узел", + "unableToUpdateNodes_few": "Невозможно обновить {{count}} узла", + "unableToUpdateNodes_many": "Невозможно обновить {{count}} узлов", + "connectionWouldCreateCycle": "Соединение создаст цикл", + "cannotConnectToSelf": "Невозможно подключиться к самому себе", + "notesDescription": "Добавляйте заметки о своем рабочем процессе", + "unknownField": "Неизвестное поле", + "colorCodeEdges": "Ребра с цветовой кодировкой", + "unknownNode": "Неизвестный узел", + "targetNodeDoesNotExist": "Недопустимое ребро: целевой/входной узел {{node}} не существует", + "unknownFieldType": "$t(nodes.unknownField) тип: {{type}}", + "collectionOrScalarFieldType": "{{name}} (Один или коллекция)", + "betaDesc": "Этот вызов находится в бета-версии. Пока он не станет стабильным, в нем могут происходить изменения при обновлении приложений. Мы планируем поддерживать этот вызов в течение длительного времени.", + "nodeVersion": "Версия узла", + "loadingNodes": "Загрузка узлов...", + "snapToGridHelp": "Привязка узлов к сетке при перемещении", + "workflowSettings": "Настройки редактора рабочих процессов", + "deletedInvalidEdge": "Удалено недопустимое ребро {{source}} -> {{target}}", + "newWorkflow": "Новый рабочий процесс", + "newWorkflowDesc": "Создать новый рабочий процесс?", + "clearWorkflow": "Очистить рабочий процесс", + "newWorkflowDesc2": "Текущий рабочий процесс имеет несохраненные изменения.", + "clearWorkflowDesc": "Очистить этот рабочий процесс и создать новый?", + "clearWorkflowDesc2": "Текущий рабочий процесс имеет несохраненные измерения.", + "viewMode": "Использовать в линейном представлении", + "editMode": "Открыть в редакторе узлов", + "resetToDefaultValue": "Сбросить к стандартному значкнию", + "edit": "Редактировать", + "noFieldsViewMode": "В этом рабочем процессе нет выбранных полей для отображения. Просмотрите полный рабочий процесс для настройки значений.", + "graph": "График", + "showEdgeLabels": "Показать метки на ребрах", + "showEdgeLabelsHelp": "Показать метки на ребрах, указывающие на соединенные узлы", + "cannotMixAndMatchCollectionItemTypes": "Невозможно смешивать и сопоставлять типы элементов коллекции", + "missingNode": "Отсутствует узел вызова", + "missingInvocationTemplate": "Отсутствует шаблон вызова", + "missingFieldTemplate": "Отсутствующий шаблон поля", + "singleFieldType": "{{name}} (Один)", + "noGraph": "Нет графика", + "imageAccessError": "Невозможно найти изображение {{image_name}}, сбрасываем на значение по умолчанию", + "boardAccessError": "Невозможно найти доску {{board_id}}, сбрасываем на значение по умолчанию", + "modelAccessError": "Невозможно найти модель {{key}}, сброс на модель по умолчанию", + "saveToGallery": "Сохранить в галерею", + "noWorkflows": "Нет рабочих процессов", + "noMatchingWorkflows": "Нет совпадающих рабочих процессов", + "workflowHelpText": "Нужна помощь? Ознакомьтесь с нашим руководством Getting Started with Workflows.", + "generatorImages_one": "{{count}} изображение", + "generatorImages_few": "{{count}} изображения", + "generatorImages_many": "{{count}} изображений", + "generatorNRandomValues_one": "{{count}} случайное значение", + "generatorNRandomValues_few": "{{count}} случайных значения", + "generatorNRandomValues_many": "{{count}} случайных значений" + }, + "boards": { + "autoAddBoard": "Коллекция для автодобавления", + "topMessage": "Этот выбор содержит изображения, используемые в следующих функциях:", + "move": "Перемещение", + "menuItemAutoAdd": "Авто добавление в эту коллекцию", + "myBoard": "Моя коллекция", + "searchBoard": "Поиск коллекции...", + "noMatching": "Нет подходящих коллекций", + "selectBoard": "Выбрать коллекцию", + "cancel": "Отменить", + "addBoard": "Добавить коллекцию", + "bottomMessage": "Удаление изображений приведёт к сбросу всех функций, которые их используют.", + "uncategorized": "Без категории", + "changeBoard": "Сменить коллекцию", + "loading": "Загрузка...", + "clearSearch": "Очистить поиск", + "deleteBoardOnly": "Удалить только коллекцию", + "movingImagesToBoard_one": "Перемещение {{count}} изображения в коллекцию:", + "movingImagesToBoard_few": "Перемещение {{count}} изображений в коллекцию:", + "movingImagesToBoard_many": "Перемещение {{count}} изображений в коллекцию:", + "downloadBoard": "Скачать коллекцию", + "deleteBoard": "Удалить коллекцию", + "deleteBoardAndImages": "Удалить коллекцию и изображения", + "deletedBoardsCannotbeRestored": "Удалённые коллекции и изображения нельзя восстановить. При выборе «Удалить только коллекцию» изображения будут перемещены в раздел «Без категории».", + "assetsWithCount_one": "{{count}} ресурс", + "assetsWithCount_few": "{{count}} ресурса", + "assetsWithCount_many": "{{count}} ресурсов", + "imagesWithCount_one": "{{count}} изображение", + "imagesWithCount_few": "{{count}} изображения", + "imagesWithCount_many": "{{count}} изображений", + "archiveBoard": "Архивировать коллекцию", + "archived": "Заархивировано", + "unarchiveBoard": "Разархивировать коллекцию", + "selectedForAutoAdd": "Выбрано для автодобавления", + "addSharedBoard": "Добавить общую коллекцию", + "boards": "Коллекции", + "addPrivateBoard": "Добавить личную коллекцию", + "private": "Личные коллекции", + "shared": "Общие коллекции", + "noBoards": "Нет коллекций {{boardType}}", + "deletedPrivateBoardsCannotbeRestored": "Удалённые коллекции и изображения нельзя восстановить. При выборе «Удалить только коллекцию» изображения будут перемещены в личный раздел «Без категории» автора изображения.", + "updateBoardError": "Ошибка обновления коллекции", + "pause": "Пауза", + "resume": "Возобновить", + "restartFailed": "Ошибка перезапуска", + "restartFile": "Перезапустить файл", + "restartRequired": "Требуется перезапуск", + "resumeRefused": "Сервер отклонил попытку возобновления. Требуется перезапуск.", + "uncategorizedImages": "Без категории", + "deleteAllUncategorizedImages": "Удалить все изображения без категории", + "deletedImagesCannotBeRestored": "Удалённые изображения нельзя восстановить.", + "hideBoards": "Скрыть коллекции", + "locateInGalery": "Показать в галерее", + "viewBoards": "Просмотреть коллекции", + "setBoardVisibility": "Установить видимость коллекции", + "setVisibilityPrivate": "Сделать приватной", + "setVisibilityPublic": "Сделать публичной", + "updateBoardVisibilityError": "Ошибка изменения видимости коллекции" + }, + "dynamicPrompts": { + "seedBehaviour": { + "perPromptDesc": "Используйте разные сиды для каждого изображения", + "perIterationLabel": "Сид на итерацию", + "perIterationDesc": "Используйте разные сиды для каждой итерации", + "perPromptLabel": "Сид для каждого изображения", + "label": "Поведение сида" + }, + "maxPrompts": "Максимум запросов", + "promptsPreview": "Предпросмотр запросов", + "dynamicPrompts": "Динамические запросы", + "loading": "Создание динамических запросов...", + "showDynamicPrompts": "Показать динамические запросы" + }, + "popovers": { + "noiseUseCPU": { + "paragraphs": [ + "Определяет, генерируется ли шум на CPU или на GPU.", + "Если включен шум CPU, определенное начальное число будет создавать одно и то же изображение на любом компьютере.", + "Включение шума CPU не влияет на производительность." + ], + "heading": "Использовать шум CPU" + }, + "paramScheduler": { + "paragraphs": [ + "Планировщик, используемый в процессе генерации.", + "Каждый планировщик определяет, как итеративно добавлять шум к изображению или как обновлять образец на основе выходных данных модели." + ], + "heading": "Планировщик" + }, + "scaleBeforeProcessing": { + "paragraphs": [ + "\"Авто\" масштабирует выбранную область до размера, наиболее подходящего для модели, до начала процесса создания изображения.", + "\"Вручную\" позволяет выбрать ширину и высоту, до которых будет масштабироваться выбранная область перед процессом создания изображения." + ], + "heading": "Масштабирование перед обработкой" + }, + "compositingMaskAdjustments": { + "heading": "Регулировка маски", + "paragraphs": [ + "Отрегулируйте маску." + ] + }, + "paramRatio": { + "heading": "Соотношение сторон", + "paragraphs": [ + "Соотношение сторон создаваемого изображения.", + "Размер изображения (в пикселях), эквивалентный 512x512, рекомендуется для моделей SD1.5, а размер, эквивалентный 1024x1024, рекомендуется для моделей SDXL." + ] + }, + "dynamicPrompts": { + "paragraphs": [ + "Динамические запросы превращают одно приглашение на множество.", + "Базовый синтакиси: \"a {red|green|blue} ball\". В итоге будет 3 запроса: \"a red ball\", \"a green ball\" и \"a blue ball\".", + "Вы можете использовать синтаксис столько раз, сколько захотите в одном запросе, но обязательно контролируйте количество генерируемых запросов с помощью параметра «Максимальное количество запросов»." + ], + "heading": "Динамические запросы" + }, + "paramVAE": { + "paragraphs": [ + "Модель, используемая для преобразования вывода AI в конечное изображение." + ], + "heading": "VAE" + }, + "paramIterations": { + "paragraphs": [ + "Количество изображений, которые нужно сгенерировать.", + "Если динамические подсказки включены, каждое из подсказок будет генерироваться столько раз." + ], + "heading": "Итерации" + }, + "paramVAEPrecision": { + "heading": "Точность VAE", + "paragraphs": [ + "Точность, используемая во время кодирования и декодирования VAE.", + "Точность Fp16/Half более эффективна за счет незначительных изменений изображения." + ] + }, + "compositingCoherenceMode": { + "heading": "Режим", + "paragraphs": [ + "Метод, используемый для создания связного изображения с вновь созданной замаскированной областью." + ] + }, + "paramSeed": { + "paragraphs": [ + "Управляет стартовым шумом, используемым для генерации.", + "Отключите опцию «Случайное», чтобы получить идентичные результаты с теми же настройками генерации." + ], + "heading": "Сид" + }, + "controlNetResizeMode": { + "heading": "Режим изменения размера", + "paragraphs": [ + "Метод подгонки размера входного изображения Control Adapter под размер выходного изображения." + ] + }, + "controlNetBeginEnd": { + "paragraphs": [ + "Эта настройка определяет, на каком этапе денойзинга (генерации) используется влияние данного слоя.", + "• Начальный шаг (%): Определяет, с какого момента в процессе генерации начинает учитываться влияние данного слоя." + ], + "heading": "Процент начала/конца шага" + }, + "dynamicPromptsSeedBehaviour": { + "paragraphs": [ + "Определяет, как используется сид при генерации промптов.", + "Для каждой итерации будет использоваться уникальный сид. Используйте это, чтобы изучить варианты запросов для одного сида.", + "Например, если у вас 5 запросов, каждое изображение будет использовать один и то же сид.", + "для каждого изображения будет использоваться уникальный сид. Это обеспечивает большую вариативность." + ], + "heading": "Поведение сида" + }, + "clipSkip": { + "paragraphs": [ + "Сколько слоев модели CLIP пропустить.", + "Некоторые модели лучше подходят для использования с CLIP Skip." + ], + "heading": "CLIP пропуск" + }, + "paramModel": { + "heading": "Модель", + "paragraphs": [ + "Модель, используемая для генерации. Разные модели обучены специализироваться на получении разных эстетических результатов и содержания." + ] + }, + "compositingCoherencePass": { + "heading": "Согласованность", + "paragraphs": [ + "Второй этап шумоподавления помогает исправить шов между изначальным изображением и перерисованной или расширенной частью." + ] + }, + "paramDenoisingStrength": { + "paragraphs": [ + "Определяет, насколько сгенерированное изображение отличается от растрового слоя (слоёв).", + "Меньшее значение сохраняет больше сходства с объединёнными видимыми растровыми слоями. Большее значение усиливает влияние глобального промпта." + ], + "heading": "Шумоподавление" + }, + "paramNegativeConditioning": { + "paragraphs": [ + "Stable Diffusion пытается избежать указанных в отрицательном запросе концепций. Используйте это, чтобы исключить качества или объекты из вывода.", + "Поддерживает синтаксис Compel и встраивания." + ], + "heading": "Негативный запрос" + }, + "compositingBlurMethod": { + "heading": "Метод размытия", + "paragraphs": [ + "Метод размытия, примененный к замаскированной области." + ] + }, + "dynamicPromptsMaxPrompts": { + "heading": "Макс. запросы", + "paragraphs": [ + "Ограничивает количество запросов, которые могут быть созданы с помощью динамических запросов." + ] + }, + "paramCFGRescaleMultiplier": { + "heading": "Множитель масштабирования CFG", + "paragraphs": [ + "Множитель масштабирования для шкалы CFG, используемый для моделей, обученных с использованием отношения сигнал/шум с нулевым терминалом (ztsnr).", + "Рекомендуемое значение 0,7 для этих моделей." + ] + }, + "infillMethod": { + "paragraphs": [ + "Метод заполнения в процессе зарисовки или перерисовки." + ], + "heading": "Метод заполнения" + }, + "controlNetWeight": { + "heading": "Вес", + "paragraphs": [ + "Определяет, насколько сильно слой влияет на процесс генерации." + ] + }, + "controlNet": { + "heading": "ControlNet", + "paragraphs": [ + "Сети ControlNets обеспечивают руководство процессом генерации, помогая создавать изображения с контролируемой композицией, структурой или стилем, в зависимости от выбранной модели." + ] + }, + "paramCFGScale": { + "heading": "Шкала точности (CFG)", + "paragraphs": [ + "Определяет, насколько сильно промпт влияет на процесс генерации.", + "Высокие значения шкалы CFG могут привести к перенасыщению и искажению результатов генерации. " + ] + }, + "controlNetControlMode": { + "paragraphs": [ + "Смещает приоритет в сторону промпта или ControlNet." + ], + "heading": "Режим управления" + }, + "paramSteps": { + "heading": "Шаги", + "paragraphs": [ + "Количество шагов, которые будут выполнены в ходе генерации.", + "Большее количество шагов обычно приводит к созданию более качественных изображений, но требует больше времени на создание." + ] + }, + "paramPositiveConditioning": { + "heading": "Запрос", + "paragraphs": [ + "Направляет процесс генерации. Вы можете использовать любые слова и фразы.", + "Большинство моделей Stable Diffusion работают только с запросом на английском языке, но бывают исключения." + ] + }, + "lora": { + "heading": "LoRA", + "paragraphs": [ + "Облегченные модели, которые используются совместно с базовыми моделями." + ] + }, + "compositingMaskBlur": { + "heading": "Размытие маски", + "paragraphs": [ + "Радиус размытия маски." + ] + }, + "compositingCoherenceMinDenoise": { + "heading": "Минимальное шумоподавление", + "paragraphs": [ + "Минимальный уровень шумоподавления для режима Coherence", + "Минимальный уровень шумоподавления для области когерентности при перерисовывании или дорисовке" + ] + }, + "compositingCoherenceEdgeSize": { + "heading": "Размер края", + "paragraphs": [ + "Размер края прохода когерентности." + ] + }, + "paramUpscaleMethod": { + "heading": "Метод увеличения", + "paragraphs": [ + "Метод, используемый для масштабирования изображения для исправления высокого разрешения." + ] + }, + "refinerCfgScale": { + "heading": "Шкала CFG", + "paragraphs": [ + "Определяет, насколько сильно промпт влияет на процесс генерации.", + "Аналогично CFG шкале генерации." + ] + }, + "controlNetProcessor": { + "heading": "Процессор", + "paragraphs": [ + "Метод обработки входного изображения для управления процессом генерации. Различные процессоры будут обеспечивать разные эффекты или стили для созданных изображений." + ] + }, + "paramHrf": { + "heading": "Включить исправление высокого разрешения", + "paragraphs": [ + "Создавайте изображения высокого качества с разрешением, превышающим оптимальное для модели. Обычно используется для предотвращения дублирования сгенерированного изображения." + ] + }, + "refinerModel": { + "paragraphs": [ + "Модель, используемая на этапе доработки в процессе генерации.", + "Аналогично модели генерации." + ], + "heading": "Модель доработчик" + }, + "refinerSteps": { + "paragraphs": [ + "Количество шагов, которые будут выполнены во время дорабатывающей части процесса генерации.", + "Похожие на шаги генерации." + ], + "heading": "Шаги" + }, + "imageFit": { + "heading": "Подогнать исходное изображение к выходному размеру", + "paragraphs": [ + "Изменяет размер исходного изображения до ширины и высоты выходного изображения. Рекомендуется включить." + ] + }, + "refinerNegativeAestheticScore": { + "heading": "Отрицательная эстетическая оценка", + "paragraphs": [ + "Поколение весов, чтобы быть более похожими на изображения с низкой эстетической оценкой, основанной на данных обучения." + ] + }, + "paramAspect": { + "heading": "Аспект", + "paragraphs": [ + "Соотношение сторон сгенерированного изображения. Изменение соотношения соответственно обновит ширину и высоту.", + "«Оптимизировать» установит оптимальные размеры ширины и высоты для выбранной модели." + ] + }, + "refinerStart": { + "heading": "Запуск доработки", + "paragraphs": [ + "Где в процессе генерации начнет использоваться доработчик.", + "0 означает, что доработчик будет использоваться на протяжении всего процесса генерации, 0,8 означает, что доработчик будет использоваться на последних 20% процесса генерации." + ] + }, + "paramWidth": { + "paragraphs": [ + "Ширина создаваемого изображения. Должно быть кратно 8." + ], + "heading": "Ширина" + }, + "patchmatchDownScaleSize": { + "heading": "Уменьшение масштаба", + "paragraphs": [ + "Насколько сильное масштабирование происходит перед заполнением.", + "Более высокое масштабирование улучшит производительность и ухудшит качество." + ] + }, + "refinerPositiveAestheticScore": { + "heading": "Положительная эстетическая оценка", + "paragraphs": [ + "Поколения веса должны быть больше похожи на изображения с высокой эстетической оценкой на основе данных обучения." + ] + }, + "refinerScheduler": { + "paragraphs": [ + "Планировщик, используемый на этапе доработки в процессе генерации.", + "Аналогично планировщику генерации." + ], + "heading": "Планировщик" + }, + "seamlessTilingXAxis": { + "heading": "Бесшовность по оси X", + "paragraphs": [ + "Плавно укладывайте изображение вдоль горизонтальной оси." + ] + }, + "loraWeight": { + "heading": "Вес", + "paragraphs": [ + "Вес LoRA. Более высокий вес приведет к большему воздействию на окончательное изображение." + ] + }, + "paramHeight": { + "paragraphs": [ + "Высота сгенерированного изображения. Должно быть кратно 8." + ], + "heading": "Высота" + }, + "seamlessTilingYAxis": { + "heading": "Бесшовность по оси Y", + "paragraphs": [ + "Плавно укладывайте изображение вдоль вертикальной оси." + ] + }, + "ipAdapterMethod": { + "heading": "Метод", + "paragraphs": [ + "Метод определяет, как референсное изображение будет влиять на процесс генерации." + ] + }, + "structure": { + "paragraphs": [ + "Структура определяет, насколько точно выходное изображение сохраняет компоновку исходного. Низкое значение допускает значительные изменения, а высокое строго сохраняет исходную композицию и расположение элементов." + ], + "heading": "Структура" + }, + "scale": { + "paragraphs": [ + "Масштаб определяет размер выходного изображения и рассчитывается как кратное разрешению исходного изображения. Например, увеличение в 2 раза для изображения 1024×1024 даст результат 2048×2048." + ], + "heading": "Масштаб" + }, + "creativity": { + "paragraphs": [ + "Креативность определяет степень свободы модели при добавлении деталей. Низкое значение сохраняет больше сходства с исходным изображением, а высокое допускает более значительные изменения. При использовании промпта высокое значение усиливает его влияние." + ], + "heading": "Креативность" + }, + "upscaleModel": { + "heading": "Модель увеличения", + "paragraphs": [ + "Модель увеличения масштаба масштабирует изображение до выходного размера перед добавлением деталей. Можно использовать любую поддерживаемую модель масштабирования, но некоторые из них специализированы для различных видов изображений, например фотографий или линейных рисунков." + ] + }, + "fluxDevLicense": { + "heading": "Некоммерческая лицензия", + "paragraphs": [ + "Модели FLUX.1 [dev] распространяются по некоммерческой лицензии FLUX [dev]. Для их коммерческого использования требуется отдельная лицензия." + ] + }, + "optimizedDenoising": { + "heading": "Оптимизированный img2img", + "paragraphs": [ + "Включите «Optimized Image-to-Image», чтобы использовать более плавную шкалу Denoise Strength для преобразований image-to-image и инпейнтинга с моделями Flux. Эта настройка улучшает контроль над степенью изменений изображения, однако её можно отключить, если вы предпочитаете стандартную шкалу Denoise Strength. Функция находится в стадии настройки и имеет статус бета-версии." + ] + }, + "paramGuidance": { + "paragraphs": [ + "Определяет, насколько сильно промпт влияет на процесс генерации.", + "Высокие значения точности могут привести к перенасыщению, а высокие или низкие значения точности могут привести к искажению результатов генерации. Точность применима только к моделям FLUX DEV." + ], + "heading": "Точность" + } + }, + "metadata": { + "positivePrompt": "Запрос", + "negativePrompt": "Негативный запрос", + "generationMode": "Режим генерации", + "Threshold": "Шумовой порог", + "metadata": "Метаданные", + "strength": "Сила img2img", + "seed": "Сид", + "imageDetails": "Детали изображения", + "model": "Модель", + "noImageDetails": "Детали изображения не найдены", + "cfgScale": "Шкала точности", + "recallParameters": "Вызов параметров", + "height": "Высота", + "noMetaData": "Метаданные не найдены", + "width": "Ширина", + "vae": "VAE", + "createdBy": "Сделано", + "workflow": "Рабочий процесс", + "steps": "Шаги", + "scheduler": "Планировщик", + "noRecallParameters": "Параметры для вызова не найдены", + "cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)", + "parameterSet": "Параметр {{parameter}} установлен", + "allPrompts": "Все запросы", + "imageDimensions": "Размеры изображения", + "canvasV2Metadata": "Слои холста", + "guidance": "Точность" + }, + "queue": { + "status": "Статус", + "pruneSucceeded": "Из очереди удалено {{item_count}} выполненных элементов", + "cancelTooltip": "Отменить текущий элемент", + "queueEmpty": "Очередь пуста", + "pauseSucceeded": "Рендеринг приостановлен", + "in_progress": "В процессе", + "queueFront": "Добавить в начало очереди", + "notReady": "Невозможно поставить в очередь", + "batchFailedToQueue": "Не удалось поставить пакет в очередь", + "completed": "Выполнено", + "queueBack": "Добавить в очередь", + "cancelFailed": "Проблема с отменой элемента", + "batchQueued": "Пакетная очередь", + "pauseFailed": "Проблема с приостановкой рендеринга", + "clearFailed": "Проблема с очисткой очереди", + "front": "передний", + "clearSucceeded": "Очередь очищена", + "pause": "Пауза", + "pruneTooltip": "Удалить {{item_count}} выполненных задач", + "cancelSucceeded": "Элемент отменен", + "batchQueuedDesc_one": "Добавлен {{count}} сеанс в {{direction}} очереди", + "batchQueuedDesc_few": "Добавлено {{count}} сеанса в {{direction}} очереди", + "batchQueuedDesc_many": "Добавлено {{count}} сеансов в {{direction}} очереди", + "graphQueued": "График поставлен в очередь", + "queue": "Очередь", + "batch": "Пакет", + "clearQueueAlertDialog": "Очистка очереди немедленно отменит все текущие задачи и очистит очередь. Ожидающие фильтры будут отменены, а область предпросмотра на холсте сброшена.", + "pending": "В ожидании", + "completedIn": "Завершено за", + "resumeFailed": "Проблема с возобновлением рендеринга", + "clear": "Очистить", + "prune": "Сократить", + "total": "Всего", + "canceled": "Отменено", + "pruneFailed": "Проблема с сокращением очереди", + "cancelBatchSucceeded": "Пакет отменен", + "clearTooltip": "Отменить все и очистить очередь", + "current": "Текущий", + "pauseTooltip": "Приостановить рендеринг", + "failed": "Неудачно", + "cancelItem": "Отменить элемент", + "next": "Следующий", + "cancelBatch": "Отменить пакет", + "back": "задний", + "cancel": "Отмена", + "session": "Сессия", + "time": "Время", + "resumeSucceeded": "Рендеринг возобновлен", + "enqueueing": "Пакетная очередь", + "resumeTooltip": "Возобновить рендеринг", + "resume": "Продолжить", + "cancelBatchFailed": "Проблема с отменой пакета", + "clearQueueAlertDialog2": "Вы уверены, что хотите очистить очередь?", + "item": "Элемент", + "graphFailedToQueue": "Не удалось поставить график в очередь", + "openQueue": "Открыть очередь", + "prompts_one": "Запрос", + "prompts_few": "Запроса", + "prompts_many": "Запросов", + "iterations_one": "Итерация", + "iterations_few": "Итерации", + "iterations_many": "Итераций", + "generations_one": "Генерация", + "generations_few": "Генерации", + "generations_many": "Генераций", + "other": "Другое", + "gallery": "Галерея", + "upscaling": "Увеличение", + "canvas": "Холст", + "generation": "Генерация", + "workflows": "Рабочие процессы", + "origin": "Источник", + "destination": "Назначение" + }, + "sdxl": { + "refinerStart": "Запуск доработчика", + "scheduler": "Планировщик", + "cfgScale": "Шкала точности (CFG)", + "noModelsAvailable": "Нет доступных моделей", + "refiner": "Доработчик", + "negAestheticScore": "Отрицательная эстетическая оценка", + "denoisingStrength": "Шумоподавление", + "refinermodel": "Дорабатывающая модель", + "posAestheticScore": "Положительная эстетическая оценка", + "loading": "Загрузка...", + "steps": "Шаги", + "refinerSteps": "Шаги доработчика" + }, + "invocationCache": { + "useCache": "Использовать кэш", + "disable": "Отключить", + "misses": "Промахи в кэше", + "enableFailed": "Проблема с включением кэша вызовов", + "invocationCache": "Кэш вызовов", + "clearSucceeded": "Кэш вызовов очищен", + "enableSucceeded": "Кэш вызовов включен", + "clearFailed": "Проблема с очисткой кэша вызовов", + "hits": "Попадания в кэш", + "disableSucceeded": "Кэш вызовов отключен", + "disableFailed": "Проблема с отключением кэша вызовов", + "enable": "Включить", + "clear": "Очистить", + "maxCacheSize": "Максимальный размер кэша", + "cacheSize": "Размер кэша" + }, + "workflows": { + "saveWorkflowAs": "Сохранить рабочий процесс как", + "workflowEditorMenu": "Меню редактора рабочего процесса", + "workflowName": "Имя рабочего процесса", + "saveWorkflow": "Сохранить рабочий процесс", + "workflowLibrary": "Библиотека схем генерации", + "downloadWorkflow": "Сохранить в файл", + "workflowSaved": "Рабочий процесс сохранен", + "unnamedWorkflow": "Безымянный рабочий процесс", + "savingWorkflow": "Сохранение рабочего процесса...", + "loading": "Загрузка рабочих процессов", + "problemSavingWorkflow": "Проблема с сохранением рабочего процесса", + "deleteWorkflow": "Удалить рабочий процесс", + "workflows": "Рабочие процессы", + "uploadWorkflow": "Загрузить из файла", + "newWorkflowCreated": "Создан новый рабочий процесс", + "saveWorkflowToProject": "Сохранить рабочий процесс в проект", + "workflowCleared": "Рабочий процесс очищен", + "noWorkflows": "Нет рабочих процессов", + "opened": "Открыто", + "updated": "Обновлено", + "ascending": "Восходящий", + "created": "Создано", + "descending": "Спуск", + "name": "Имя", + "loadWorkflow": "Рабочий процесс $t(common.load)", + "convertGraph": "Конвертировать график", + "loadFromGraph": "Загрузка рабочего процесса из графика", + "autoLayout": "Автоматическое расположение", + "deleteWorkflow2": "Вы уверены, что хотите удалить этот рабочий процесс? Это нельзя отменить.", + "chooseWorkflowFromLibrary": "Выбрать рабочий процесс из библиотеки", + "edit": "Редактировать", + "download": "Скачать", + "copyShareLink": "Скопировать ссылку на общий доступ", + "copyShareLinkForWorkflow": "Скопировать ссылку на общий доступ для рабочего процесса", + "delete": "Удалить" + }, + "hrf": { + "metadata": { + "strength": "Сила исправления высокого разрешения", + "enabled": "Исправление высокого разрешения включено", + "method": "Метод исправления высокого разрешения" + }, + "hrf": "Исправление высокого разрешения" + }, + "models": { + "noMatchingModels": "Нет подходящих моделей", + "loading": "загрузка", + "noModelsAvailable": "Нет доступных моделей", + "addLora": "Добавить LoRA", + "selectModel": "Выберите модель", + "noRefinerModelsInstalled": "Дорабатывающие модели SDXL не установлены", + "lora": "LoRA", + "defaultVAE": "Стандартное VAE", + "concepts": "LoRA" + }, + "accordions": { + "compositing": { + "infillTab": "Заполнение", + "coherenceTab": "Согласованность", + "title": "Композиция" + }, + "control": { + "title": "Контроль" + }, + "generation": { + "title": "Генерация" + }, + "advanced": { + "title": "Расширенные", + "options": "Опции $t(accordions.advanced.title)" + }, + "image": { + "title": "Изображение" + } + }, + "prompt": { + "addPromptTrigger": "Добавить триггер запроса", + "compatibleEmbeddings": "Совместимые встраивания", + "noMatchingTriggers": "Нет соответствующих триггеров" + }, + "controlLayers": { + "moveToBack": "На задний план", + "moveForward": "Переместить вперёд", + "moveBackward": "Переместить назад", + "autoNegative": "Авто негатив", + "rectangle": "Прямоугольник", + "addNegativePrompt": "Добавить $t(controlLayers.negativePrompt)", + "regionalGuidance": "Региональное влияние", + "opacity": "Непрозрачность", + "addLayer": "Добавить слой", + "moveToFront": "На передний план", + "addPositivePrompt": "Добавить $t(controlLayers.prompt)", + "regional": "Региональный", + "bookmark": "Закладка для быстрого переключения", + "fitBboxToLayers": "Подогнать рамку к слоям", + "mergeVisibleOk": "Объединенные слои", + "mergeVisibleError": "Ошибка объединения слоев", + "clearHistory": "Очистить историю", + "mergeVisible": "Объединить видимые", + "removeBookmark": "Удалить закладку", + "saveLayerToAssets": "Сохранить слой в ресурсы", + "clearCaches": "Очистить кэши", + "recalculateRects": "Пересчитать прямоугольники", + "saveBboxToGallery": "Сохранить область в галерею", + "canvas": "Холст", + "global": "Глобальный", + "newGlobalReferenceImageError": "Проблема с созданием глобального референсного изображения", + "newRegionalReferenceImageOk": "Создано региональное референсное изображение", + "newRegionalReferenceImageError": "Проблема создания регионального референсного изображения", + "newControlLayerOk": "Создан слой управления", + "newControlLayerError": "Ошибка создания слоя управления", + "newRasterLayerOk": "Создан растровый слой", + "newRasterLayerError": "Ошибка создания растрового слоя", + "newGlobalReferenceImageOk": "Создано глобальное референсное изображение", + "bboxOverlay": "Показать наложение рамки", + "saveCanvasToGallery": "Сохранить холст в галерею", + "pullBboxIntoReferenceImageOk": "Рамка перенесена в референсное изображение", + "pullBboxIntoReferenceImageError": "Ошибка переноса рамки в референсное изображение", + "regionIsEmpty": "Выбранный регион пуст", + "savedToGalleryOk": "Сохранено в галерею", + "savedToGalleryError": "Ошибка сохранения в галерею", + "pullBboxIntoLayerOk": "Содержимое рамки перенесено в слой", + "pullBboxIntoLayerError": "Проблема с переносом рамки в слой", + "newLayerFromImage": "Новый слой из изображения", + "filter": { + "lineart_anime_edge_detection": { + "label": "Обнаружение краев Lineart Anime", + "description": "Создает карту краев выбранного слоя с помощью модели обнаружения краев Lineart Anime." + }, + "hed_edge_detection": { + "scribble": "Штрих", + "label": "обнаружение границ HED", + "description": "Создает карту границ из выбранного слоя с использованием модели обнаружения границ HED." + }, + "mlsd_detection": { + "description": "Генерирует карту сегментов линий из выбранного слоя с помощью модели обнаружения сегментов линий MLSD.", + "score_threshold": "Пороговый балл", + "distance_threshold": "Порог расстояния", + "label": "Обнаружение сегментов линии" + }, + "canny_edge_detection": { + "low_threshold": "Низкий порог", + "high_threshold": "Высокий порог", + "label": "Обнаружение краев", + "description": "Создает карту краев выбранного слоя с помощью алгоритма обнаружения краев Canny." + }, + "color_map": { + "description": "Создайте цветовую карту из выбранного слоя.", + "label": "Цветная карта", + "tile_size": "Размер плитки" + }, + "depth_anything_depth_estimation": { + "model_size_base": "Базовая", + "model_size_large": "Большая", + "label": "Анализ глубины", + "model_size_small": "Маленькая", + "model_size_small_v2": "Маленькая v2", + "description": "Создает карту глубины из выбранного слоя с использованием модели Depth Anything.", + "model_size": "Размер модели" + }, + "mediapipe_face_detection": { + "min_confidence": "Минимальная уверенность", + "label": "Распознавание лиц MediaPipe", + "description": "Обнаруживает лица в выбранном слое с помощью модели обнаружения лиц MediaPipe.", + "max_faces": "Максимум лиц" + }, + "lineart_edge_detection": { + "label": "Обнаружение краев Lineart", + "description": "Создает карту краев выбранного слоя с помощью модели обнаружения краев Lineart.", + "coarse": "Грубый" + }, + "filterType": "Тип фильтра", + "autoProcess": "Автообработка", + "reset": "Сбросить", + "content_shuffle": { + "scale_factor": "Коэффициент", + "label": "Перетасовка контента", + "description": "Перемешивает содержимое выбранного слоя, аналогично эффекту «сжижения»." + }, + "dw_openpose_detection": { + "label": "Обнаружение DW Openpose", + "draw_hands": "Рисовать руки", + "description": "Обнаруживает позы человека в выбранном слое с помощью модели DW Openpose.", + "draw_face": "Рисовать лицо", + "draw_body": "Рисовать тело" + }, + "normal_map": { + "label": "Карта нормалей", + "description": "Создает карту нормалей для выбранного слоя." + }, + "spandrel_filter": { + "model": "Модель", + "label": "Модель img2img", + "autoScale": "Авто масштабирование", + "scale": "Целевой масштаб", + "description": "Запустить модель изображения к изображению на выбранном слое.", + "autoScaleDesc": "Выбранная модель будет работать до тех пор, пока не будет достигнут целевой масштаб." + }, + "pidi_edge_detection": { + "scribble": "Штрих", + "description": "Генерирует карту краев из выбранного слоя с помощью модели обнаружения краев PiDiNet.", + "label": "Обнаружение краев PiDiNet", + "quantize_edges": "Квантизация краев" + }, + "process": "Обработать", + "apply": "Применить", + "cancel": "Отменить", + "filter": "Фильтр", + "filters": "Фильтры" + }, + "HUD": { + "entityStatus": { + "isHidden": "{{title}} скрыт", + "isLocked": "{{title}} заблокирован", + "isDisabled": "{{title}} отключен", + "isEmpty": "{{title}} пуст", + "isFiltering": "{{title}} фильтруется", + "isTransforming": "{{title}} трансформируется" + }, + "scaledBbox": "Масштабированная рамка", + "bbox": "Ограничительная рамка", + "textSessionActive": "Активен режим ввода" + }, + "canvasContextMenu": { + "saveBboxToGallery": "Сохранить рамку в галерею", + "newGlobalReferenceImage": "Новое глобальное референсное изображение", + "bboxGroup": "Сохдать из рамки", + "canvasGroup": "Холст", + "newControlLayer": "Новый контрольный слой", + "newRasterLayer": "Новый растровый слой", + "saveToGalleryGroup": "Сохранить в галерею", + "saveCanvasToGallery": "Сохранить холст в галерею", + "cropCanvasToBbox": "Обрезать холст по рамке", + "newRegionalReferenceImage": "Новое региональное эталонное изображение" + }, + "fill": { + "solid": "Сплошной", + "fillStyle": "Стиль заливки", + "fillColor": "Цвет заливкии", + "grid": "Сетка", + "horizontal": "Горизонтальная", + "diagonal": "Диагональная", + "crosshatch": "Штриховка", + "vertical": "Вертикальная" + }, + "showHUD": "Показать HUD", + "copyToClipboard": "Копировать в буфер обмена", + "ipAdapterMethod": { + "composition": "Только композиция", + "style": "Только стиль", + "ipAdapterMethod": "Метод IP адаптера", + "full": "Полный" + }, + "addReferenceImage": "Добавить $t(controlLayers.referenceImage)", + "inpaintMask": "Маска перерисовки", + "sendToCanvas": "Отправить на холст", + "regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)", + "regionalGuidance_withCount_few": "Региональных влияния", + "regionalGuidance_withCount_many": "Региональных влияний", + "controlLayer_withCount_one": "$t(controlLayers.controlLayer)", + "controlLayer_withCount_few": "Контрольных слоя", + "controlLayer_withCount_many": "Контрольных слоев", + "newCanvasFromImage": "Новый холст из изображения", + "inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)", + "inpaintMask_withCount_few": "Маски перерисовки", + "inpaintMask_withCount_many": "Масок перерисовки", + "controlMode": { + "prompt": "Промпт", + "controlMode": "Режим контроля", + "megaControl": "Максимальный контроль", + "balanced": "Сбалансированный", + "control": "Контроль" + }, + "settings": { + "isolatedPreview": "Изолированный предпросмотр", + "invertBrushSizeScrollDirection": "Инвертировать прокрутку для размера кисти", + "snapToGrid": { + "label": "Привязка к сетке", + "on": "Вкл", + "off": "Выкл" + }, + "pressureSensitivity": "Чувствительность к давлению", + "isolatedStagingPreview": "Изолированный предпросмотр на промежуточной стадии", + "preserveMask": { + "label": "Сохранить замаскированную область", + "alert": "Сохранение замаскированной области" + } + }, + "stagingArea": { + "discardAll": "Отбросить все", + "discard": "Отбросить", + "accept": "Принять", + "previous": "Предыдущий", + "next": "Следующий", + "saveToGallery": "Сохранить в галерею", + "showResultsOn": "Показать результаты", + "showResultsOff": "Скрыть результаты" + }, + "pullBboxIntoReferenceImage": "Преобразовать рамку в референсное изображение", + "enableAutoNegative": "Включить авто негатив", + "maskFill": "Заливка маски", + "tool": { + "move": "Перемещение", + "bbox": "Ограничительная рамка", + "view": "Перемещение холста", + "brush": "Кисть", + "eraser": "Ластик", + "rectangle": "Прямоугольник", + "colorPicker": "Пипетка", + "text": "Текст" + }, + "rasterLayer": "Растровый слой", + "enableTransparencyEffect": "Включить эффект прозрачности", + "hidingType": "Скрыть {{type}}", + "addRegionalGuidance": "Добавить $t(controlLayers.regionalGuidance)", + "deleteSelected": "Удалить выбранное", + "pullBboxIntoLayer": "Преобразовать рамку в слой", + "locked": "Заблокировано", + "replaceLayer": "Заменить слой", + "width": "Ширина", + "controlLayer": "Слой управления", + "addRasterLayer": "Добавить $t(controlLayers.rasterLayer)", + "addControlLayer": "Добавить $t(controlLayers.controlLayer)", + "addInpaintMask": "Добавить $t(controlLayers.inpaintMask)", + "cropLayerToBbox": "Обрезать слой по рамке", + "clipToBbox": "Ограничить мазки рамкой", + "outputOnlyMaskedRegions": "Выводить только сгенерированные области", + "duplicate": "Дублировать", + "layer_one": "Слой", + "layer_few": "Слоя", + "layer_many": "Слоев", + "prompt": "Промпт", + "negativePrompt": "Негативный промпт", + "beginEndStepPercentShort": "Начало/конец %", + "transform": { + "transform": "Трансформировать", + "fitToBbox": "Вместить в рамку", + "reset": "Сбросить", + "apply": "Применить", + "cancel": "Отменить", + "fitModeContain": "Уместить", + "fitMode": "Режим подгонки", + "fitModeFill": "Заполнить" + }, + "disableAutoNegative": "Отключить авто негатив", + "deleteReferenceImage": "Удалить референсное изображение", + "rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)", + "rasterLayer_withCount_few": "Растровых слоя", + "rasterLayer_withCount_many": "Растровых слоев", + "transparency": "Прозрачность", + "weight": "Вес", + "disableTransparencyEffect": "Отключить эффект прозрачности", + "showingType": "Показать {{type}}", + "dynamicGrid": "Динамическая сетка", + "logDebugInfo": "Писать отладочную информацию", + "unlocked": "Разблокировано", + "showProgressOnCanvas": "Показать прогресс на холсте", + "regionalReferenceImage": "Региональное референсное изображение", + "globalReferenceImage": "Глобальное референсное изображение", + "referenceImage": "Референсное изображение", + "text": { + "px": "px", + "alignRight": "По правому краю", + "alignCenter": "По центру", + "alignLeft": "По левому краю", + "strikethrough": "Зачёркнутый", + "italic": "Курсив", + "bold": "Полужирный", + "size": "Размер", + "font": "Шрифт" + }, + "newImg2ImgCanvasFromImage": "Новое изображение из Img2Img", + "sendToCanvasDesc": "При нажатии Invoke результат появляется на холсте в режиме предпросмотра.", + "compositeOperation": { + "blendModes": { + "darken": "Затемнение", + "multiply": "Умножение", + "color-dodge": "Осветление основы", + "color-burn": "Затемнение основы", + "screen": "Экран", + "hard-light": "Жёсткий свет", + "soft-light": "Мягкий свет", + "overlay": "Перекрытие", + "hue": "Тон", + "color": "Цвет", + "source-over": "Обычный" + } + }, + "globalReferenceImage_withCount_one": "$t(controlLayers.globalReferenceImage)", + "globalReferenceImage_withCount_few": "Глобальных референсных изображения", + "globalReferenceImage_withCount_many": "Глобальных референсных изображений", + "regionalGuidance_withCount_hidden": "Региональное влияние (скрыто: {{count}})", + "controlLayers_withCount_hidden": "Слои управления (скрыто: {{count}})" + }, + "ui": { + "tabs": { + "canvas": "Холст", + "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)", + "models": "Модели", + "workflows": "Рабочие процессы", + "modelsTab": "$t(ui.tabs.models) $t(common.tab)", + "queue": "Очередь", + "upscaling": "Увеличение", + "upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)", + "gallery": "Галерея" + } + }, + "upscaling": { + "exceedsMaxSize": "Параметры масштабирования превышают максимальный размер", + "exceedsMaxSizeDetails": "Максимальный предел масштабирования составляет {{maxUpscaleDimension}}x{{maxUpscaleDimension}} пикселей. Пожалуйста, попробуйте использовать меньшее изображение или уменьшите масштаб.", + "structure": "Структура", + "missingTileControlNetModel": "Не установлены подходящие модели ControlNet", + "missingUpscaleInitialImage": "Отсутствует увеличиваемое изображение", + "missingUpscaleModel": "Отсутствует увеличивающая модель", + "creativity": "Креативность", + "upscaleModel": "Модель увеличения", + "scale": "Масштаб", + "mainModelDesc": "Основная модель (архитектура SD1.5 или SDXL)", + "upscaleModelDesc": "Модель увеличения (img2img)", + "postProcessingModel": "Модель постобработки", + "tileControlNetModelDesc": "Модель ControlNet для выбранной архитектуры основной модели", + "missingModelsWarning": "Зайдите в Менеджер моделей чтоб установить необходимые модели:", + "postProcessingMissingModelWarning": "Посетите Менеджер моделей, чтобы установить модель постобработки (img2img).", + "upscale": "Увеличить" + }, + "stylePresets": { + "noMatchingTemplates": "Нет подходящих шаблонов", + "promptTemplatesDesc1": "Шаблоны подсказок добавляют текст к подсказкам, которые вы пишете в окне подсказок.", + "sharedTemplates": "Общие шаблоны", + "templateDeleted": "Шаблон запроса удален", + "toggleViewMode": "Переключить режим просмотра", + "type": "Тип", + "unableToDeleteTemplate": "Не получилось удалить шаблон запроса", + "viewModeTooltip": "Вот как будет выглядеть ваш запрос с выбранным шаблоном. Чтобы его отредактировать, щелкните в любом месте текстового поля.", + "viewList": "Просмотреть список шаблонов", + "active": "Активно", + "choosePromptTemplate": "Выберите шаблон запроса", + "defaultTemplates": "Стандартные шаблоны", + "deleteImage": "Удалить изображение", + "deleteTemplate": "Удалить шаблон", + "deleteTemplate2": "Вы уверены, что хотите удалить этот шаблон? Это нельзя отменить.", + "editTemplate": "Редактировать шаблон", + "exportPromptTemplates": "Экспорт моих шаблонов запроса (CSV)", + "exportDownloaded": "Экспорт скачан", + "exportFailed": "Невозможно сгенерировать и загрузить CSV", + "flatten": "Объединить выбранный шаблон с текущим запросом", + "acceptedColumnsKeys": "Принимаемые столбцы/ключи:", + "positivePromptColumn": "'prompt' или 'positive_prompt'", + "insertPlaceholder": "Вставить заполнитель", + "name": "Имя", + "negativePrompt": "Негативный запрос", + "promptTemplatesDesc3": "Если вы не используете заполнитель, шаблон будет добавлен в конец запроса.", + "positivePrompt": "Позитивный запрос", + "preview": "Предпросмотр", + "private": "Приватный", + "updatePromptTemplate": "Обновить шаблон запроса", + "uploadImage": "Загрузить изображение", + "useForTemplate": "Использовать для шаблона запроса", + "clearTemplateSelection": "Очистить выбор шаблона", + "copyTemplate": "Копировать шаблон", + "createPromptTemplate": "Создать шаблон запроса", + "importTemplates": "Импортировать шаблоны запроса (CSV/JSON)", + "nameColumn": "'name'", + "negativePromptColumn": "'negative_prompt'", + "myTemplates": "Мои шаблоны", + "noTemplates": "Нет шаблонов", + "promptTemplatesDesc2": "Используйте строку-заполнитель
{{placeholder}}
, чтобы указать место, куда должен быть включен ваш запрос в шаблоне.", + "searchByName": "Поиск по имени", + "shared": "Общий", + "promptTemplateCleared": "Шаблон запроса создан" + }, + "system": { + "logNamespaces": { + "canvas": "Холст", + "config": "Конфигурация", + "generation": "Генерация", + "workflows": "Рабочие процессы", + "gallery": "Галерея", + "models": "Модели", + "logNamespaces": "Пространства имен логов", + "events": "События", + "system": "Система", + "queue": "Очередь", + "metadata": "Метаданные" + }, + "enableLogging": "Включить логи", + "logLevel": { + "logLevel": "Уровень логов", + "fatal": "Фатальное", + "debug": "Отладка", + "info": "Инфо", + "warn": "Предупреждение", + "error": "Ошибки", + "trace": "Трассировка" + } + }, + "whatsNew": { + "whatsNewInInvoke": "Что нового в Invoke" + }, + "newUserExperience": { + "toGetStarted": "Чтобы начать работу, введите в поле запрос и нажмите Invoke, чтобы сгенерировать первое изображение. Выберите шаблон запроса, чтобы улучшить результаты. Вы можете сохранить изображения непосредственно в Галерею или отредактировать их на Холсте.", + "gettingStartedSeries": "Хотите получить больше рекомендаций? Ознакомьтесь с нашей серией Getting Started Series для получения советов по раскрытию всего потенциала Invoke Studio." + }, + "auth": { + "login": { + "title": "Войти в InvokeAI", + "password": "Пароль", + "passwordPlaceholder": "Пароль", + "email": "Почта", + "emailPlaceholder": "Почта", + "rememberMe": "Запомнить на 7 дней", + "signIn": "Войти", + "signingIn": "Вход...", + "loginFailed": "Ошибка логина. Проверьте введенные данные.", + "sessionExpired": "Срок действия учетных данных истек. Войдите в систему заново, чтобы продолжить." + }, + "setup": { + "title": "Добро пожаловать в InvokeAI", + "subtitle": "Чтобы начать, настройте главную учетную запись", + "email": "Почта", + "emailPlaceholder": "admin@example.com", + "emailHelper": "Это будет вашим логином для входа", + "displayName": "Отображаемое имя", + "displayNamePlaceholder": "Администратор", + "displayNameHelper": "Ваше имя, как оно будет отображаться в приложении", + "password": "Пароль", + "passwordPlaceholder": "Пароль", + "passwordHelper": "Должно быть не менее 8 символов, включая заглавные и строчные буквы, а также цифры", + "passwordTooShort": "Пароль должен содержать хотя бы 8 символов", + "passwordMissingRequirements": "Пароль должен содержать заглавные и строчные буквы, а также цифры", + "confirmPassword": "Подтвердите пароль", + "confirmPasswordPlaceholder": "Подтвердите пароль", + "passwordsDoNotMatch": "Пароли не сходятся", + "createAccount": "Создать главный аккаунт", + "creatingAccount": "Настройка...", + "setupFailed": "Ошибка настройки. Пожалуйста, попробуйте ещё раз.", + "passwordHelperRelaxed": "Введите любой пароль (отобразится сложность)" + }, + "userMenu": "Меню", + "admin": "Администратор", + "logout": "Выйти", + "adminOnlyFeature": "Эта функция доступна только администраторам.", + "profile": { + "menuItem": "Мой профиль", + "title": "Мой профиль", + "email": "Почта", + "emailReadOnly": "Почта не может быть изменена", + "displayName": "Отображаемое имя", + "displayNamePlaceholder": "Ваше имя", + "changePassword": "Изменить пароль", + "currentPassword": "Текущий пароль", + "currentPasswordPlaceholder": "Текущий пароль", + "newPassword": "Новый пароль", + "newPasswordPlaceholder": "Новый пароль", + "confirmPassword": "Подтвердите новый пароль", + "confirmPasswordPlaceholder": "Подтвердите новый пароль", + "passwordsDoNotMatch": "Пароли не сходятся", + "saveSuccess": "Профиль успешно обновлен", + "saveFailed": "Ошибка сохранения профиля. Пожалуйста, попробуйте снова." + }, + "userManagement": { + "menuItem": "Управление пользователями", + "title": "Управление пользователями", + "email": "Почта", + "emailPlaceholder": "user@example.com", + "displayName": "Отображаемое имя", + "displayNamePlaceholder": "Отображаемое имя", + "password": "Пароль", + "passwordPlaceholder": "Пароль", + "newPassword": "Новый пароль", + "newPasswordPlaceholder": "Оставьте пустым, чтобы не менять пароль", + "role": "Роль", + "status": "Статус", + "actions": "Действия", + "isAdmin": "Администратор", + "user": "Пользователь", + "you": "Вы", + "createUser": "Создать пользователя", + "editUser": "Изменить пользователя", + "deleteUser": "Удалить пользователя", + "deleteConfirm": "Вы точно хотите удалить \"{{name}}\"? Это необратимое действие.", + "generatePassword": "Сгенерировать сильный пароль", + "showPassword": "Показать пароль", + "hidePassword": "Скрыть пароль", + "activate": "Включить", + "deactivate": "Отключить", + "saveFailed": "Не получилось сохранить пользователя. Пожалуйста, попробуйте ещё раз.", + "deleteFailed": "Не получилось удалить пользователя. Пожалуйста, попробуйте ещё раз.", + "loadFailed": "Не получилось загрузить пользователей.", + "back": "Назад", + "cannotDeleteSelf": "Вы не можете удалить свой аккаунт", + "cannotDeactivateSelf": "Вы не можете отключить свой аккаунт" + }, + "passwordStrength": { + "weak": "Слабый пароль", + "moderate": "Средний пароль", + "strong": "Сложный пароль" + } + } +} diff --git a/invokeai/frontend/web/public/locales/sv.json b/invokeai/frontend/web/public/locales/sv.json new file mode 100644 index 00000000000..512626f1e2f --- /dev/null +++ b/invokeai/frontend/web/public/locales/sv.json @@ -0,0 +1,33 @@ +{ + "accessibility": { + "uploadImage": "Ladda upp bild", + "invokeProgressBar": "Invoke förloppsmätare", + "nextImage": "Nästa bild", + "reset": "Starta om", + "previousImage": "Föregående bild" + }, + "common": { + "hotkeysLabel": "Snabbtangenter", + "reportBugLabel": "Rapportera bugg", + "githubLabel": "Github", + "discordLabel": "Discord", + "settingsLabel": "Inställningar", + "upload": "Ladda upp", + "cancel": "Avbryt", + "accept": "Acceptera", + "statusDisconnected": "Frånkopplad", + "loading": "Laddar", + "languagePickerLabel": "Språkväljare", + "txt2img": "Text till bild", + "nodes": "Noder", + "img2img": "Bild till bild", + "postprocessing": "Efterbehandling", + "load": "Ladda", + "back": "Bakåt" + }, + "gallery": { + "galleryImageSize": "Bildstorlek", + "gallerySettings": "Galleriinställningar", + "autoSwitchNewImages": "Ändra automatiskt till nya bilder" + } +} diff --git a/invokeai/frontend/web/public/locales/tr.json b/invokeai/frontend/web/public/locales/tr.json new file mode 100644 index 00000000000..a18da1dca59 --- /dev/null +++ b/invokeai/frontend/web/public/locales/tr.json @@ -0,0 +1,397 @@ +{ + "accessibility": { + "invokeProgressBar": "Invoke durum çubuğu", + "nextImage": "Sonraki Görsel", + "reset": "Resetle", + "uploadImage": "Görsel Yükle", + "previousImage": "Önceki Görsel", + "menu": "Menü", + "about": "Hakkında", + "mode": "Kip", + "resetUI": "$t(accessibility.reset)Arayüz", + "createIssue": "Sorun Bildir" + }, + "common": { + "hotkeysLabel": "Kısayol Tuşları", + "languagePickerLabel": "Dil", + "reportBugLabel": "Sorun Bildir", + "githubLabel": "Github", + "discordLabel": "Discord", + "settingsLabel": "Seçenekler", + "txt2img": "Yazıdan Görsel", + "img2img": "Görselden Görsel", + "linear": "Doğrusal", + "nodes": "İş Akışı Düzenleyici", + "postprocessing": "Rötuş", + "batch": "Toplu İş Yöneticisi", + "accept": "Onayla", + "cancel": "Vazgeç", + "advanced": "Gelişmiş", + "copyError": "$t(gallery.copy) Hata", + "on": "Açık", + "or": "ya da", + "aboutDesc": "Invoke'u iş için mi kullanıyorsunuz? Şuna bir göz atın:", + "ai": "yapay zeka", + "auto": "Otomatik", + "communityLabel": "Topluluk", + "back": "Geri", + "areYouSure": "Emin misiniz?", + "openInNewTab": "Yeni Sekmede Aç", + "aboutHeading": "Yaratıcı Gücünüzün Sahibi Olun", + "load": "Yükle", + "loading": "Yükleniyor", + "inpaint": "içboyama", + "modelManager": "Model Yöneticisi", + "orderBy": "Sırala", + "outpaint": "dışboyama", + "outputs": "Çıktılar", + "learnMore": "Bilgi Edin", + "save": "Kaydet", + "random": "Rastgele", + "simple": "Basit", + "template": "Şablon", + "saveAs": "Farklı Kaydet", + "somethingWentWrong": "Bir sorun oluştu", + "statusDisconnected": "Bağlantı Kesildi", + "unknown": "Bilinmeyen", + "green": "Yeşil", + "red": "Kırmızı", + "blue": "Mavi", + "alpha": "Alfa", + "file": "Dosya", + "folder": "Klasör", + "format": "biçim", + "details": "Ayrıntılar", + "error": "Hata", + "safetensors": "Safetensors", + "upload": "Yükle", + "dontAskMeAgain": "Bir daha sorma", + "delete": "Kaldır", + "direction": "Yön", + "unknownError": "Bilinmeyen Hata", + "installed": "Yüklü", + "data": "Veri", + "input": "Giriş", + "copy": "Kopyala", + "created": "Yaratma", + "updated": "Güncelleme", + "ipAdapter": "IP Aracı", + "t2iAdapter": "T2I Aracı", + "controlNet": "ControlNet" + }, + "accordions": { + "generation": { + "title": "Oluşturma" + }, + "image": { + "title": "Görsel" + }, + "advanced": { + "title": "Gelişmiş" + }, + "compositing": { + "title": "Birleştirme", + "coherenceTab": "Uyum Geçişi", + "infillTab": "Doldurma" + }, + "control": { + "title": "Yönetim" + } + }, + "boards": { + "autoAddBoard": "Panoya Otomatik Ekleme", + "cancel": "Vazgeç", + "clearSearch": "Aramayı Sil", + "deleteBoard": "Panoyu Sil", + "loading": "Yükleniyor...", + "myBoard": "Panom", + "selectBoard": "Bir Pano Seç", + "addBoard": "Pano Ekle", + "deleteBoardAndImages": "Panoyu ve Görselleri Sil", + "deleteBoardOnly": "Sadece Panoyu Sil", + "deletedBoardsCannotbeRestored": "Silinen panolar geri getirilemez", + "menuItemAutoAdd": "Bu panoya otomatik olarak ekle", + "move": "Taşı", + "movingImagesToBoard_one": "{{count}} görseli şu panoya taşı:", + "movingImagesToBoard_other": "{{count}} görseli şu panoya taşı:", + "noMatching": "Eşleşen pano yok", + "searchBoard": "Pano Ara...", + "topMessage": "Bu pano, şuralarda kullanılan görseller içeriyor:", + "downloadBoard": "Panoyu İndir", + "uncategorized": "Kategorisiz", + "changeBoard": "Panoyu Değiştir", + "bottomMessage": "Bu panoyu ve görselleri silmek, bunları kullanan özelliklerin resetlemesine neden olacaktır." + }, + "queue": { + "resumeSucceeded": "İşlem Sürdürüldü", + "openQueue": "Sırayı Göster", + "cancelSucceeded": "İş Geri Çekildi", + "cancelFailed": "İşi Geri Çekmede Sorun", + "prune": "Arındır", + "pruneTooltip": "{{item_count}} Bitmiş İşi Sil", + "resumeFailed": "İşlemi Sürdürmede Sorun", + "pauseFailed": "İşlemi Duraklatmada Sorun", + "cancelBatchSucceeded": "Toplu İşten Vazgeçildi", + "pruneSucceeded": "{{item_count}} Bitmiş İş Sıradan Silindi", + "in_progress": "İşleniyor", + "completed": "Bitti", + "canceled": "Vazgeçildi", + "back": "arka", + "queueFront": "Sıranın Başına Ekle", + "queueBack": "Sıraya Ekle", + "resumeTooltip": "İşlemi Sürdür", + "clearQueueAlertDialog2": "Sırayı boşaltmak istediğinizden emin misiniz?", + "batchQueuedDesc_one": "{{count}} iş sıranın {{direction}} eklendi", + "batchQueuedDesc_other": "{{count}} iş sıranın {{direction}} eklendi", + "batchFailedToQueue": "Toplu İş Sıraya Alınamadı", + "front": "ön", + "queue": "Sıra", + "resume": "Sürdür", + "queueEmpty": "Sıra Boş", + "clearQueueAlertDialog": "Sırayı boşaltma düğmesi geçerli işlemi durdurur ve sırayı boşaltır.", + "current": "Şimdiki", + "time": "Süre", + "pause": "Duraklat", + "pauseTooltip": "İşlemi Duraklat", + "pruneFailed": "Sırayı Arındırmada Sorun", + "clearTooltip": "Vazgeç ve Tüm İşleri Sil", + "clear": "Boşalt", + "cancelBatchFailed": "Toplu İşten Vazgeçmede Sorun", + "next": "Sonraki", + "status": "Durum", + "failed": "Başarısız", + "item": "İş", + "enqueueing": "Toplu İş Sıraya Alınıyor", + "pauseSucceeded": "İşlem Duraklatıldı", + "cancel": "Vazgeç", + "cancelTooltip": "Bu İşi Geri Çek", + "clearSucceeded": "Sıra Boşaltıldı", + "clearFailed": "Sırayı Boşaltmada Sorun", + "cancelBatch": "Toplu İşten Vazgeç", + "cancelItem": "İşi Geri Çek", + "total": "Toplam", + "pending": "Sırada", + "completedIn": "'de bitirildi", + "batch": "Toplu İş", + "session": "Oturum", + "batchQueued": "Toplu İş Sıraya Alındı", + "notReady": "Sıraya Alınamadı", + "graphFailedToQueue": "Çizge sıraya alınamadı", + "graphQueued": "Çizge sıraya alındı" + }, + "invocationCache": { + "cacheSize": "Önbellek Boyutu", + "disable": "Kapat", + "clear": "Boşalt", + "maxCacheSize": "Maksimum Önbellek Boyutu", + "useCache": "Önbellek Kullan", + "enable": "Aç" + }, + "gallery": { + "deleteImagePermanent": "Silinen görseller geri getirilemez.", + "autoAssignBoardOnClick": "Tıklanan Panoya Otomatik Atama", + "loading": "Yükleniyor", + "starImage": "Yıldız Koy", + "download": "İndir", + "deleteSelection": "Seçileni Sil", + "featuresWillReset": "Bu görseli silerseniz, o özellikler resetlenecektir.", + "noImageSelected": "Görsel Seçilmedi", + "unstarImage": "Yıldızı Kaldır", + "gallerySettings": "Galeri Düzeni", + "image": "görsel", + "galleryImageSize": "Görsel Boyutu", + "copy": "Kopyala", + "autoSwitchNewImages": "Yeni Görseli Biter Bitmez Gör", + "currentlyInUse": "Bu görsel şurada kullanımda:", + "deleteImage_one": "Görseli Sil", + "deleteImage_other": "", + "downloadSelection": "Seçileni İndir", + "dropOrUpload": "$t(gallery.drop) ya da Yükle", + "dropToUpload": "Yüklemek için $t(gallery.drop)", + "drop": "Bırak" + }, + "hrf": { + "hrf": "Yüksek Çözünürlük Kürü", + "metadata": { + "enabled": "Yüksek Çözünürlük Kürü Açık", + "strength": "Yüksek Çözünürlük Kürü Etkisi", + "method": "Yüksek Çözünürlük Kürü Yöntemi" + } + }, + "hotkeys": { + "noHotkeysFound": "Kısayol Tuşu Bulanamadı", + "searchHotkeys": "Kısayol Tuşlarında Ara", + "clearSearch": "Aramayı Sil" + }, + "nodes": { + "unableToValidateWorkflow": "İş Akışı Doğrulanamadı", + "workflowContact": "İletişim", + "loadWorkflow": "İş Akışı Yükle", + "workflowNotes": "Notlar", + "workflow": "İş Akışı", + "notesDescription": "İş akışınız hakkında not düşün", + "workflowTags": "Etiketler", + "workflowDescription": "Kısa Tanım", + "workflowValidation": "İş Akışı Doğrulama Sorunu", + "workflowVersion": "Sürüm", + "newWorkflow": "Yeni İş Akışı", + "currentImageDescription": "İşlemdeki görseli Çizge Düzenleyicide gösterir", + "workflowAuthor": "Yaratıcı", + "workflowName": "Ad", + "workflowSettings": "İş Akışı Düzenleyici Seçenekleri", + "currentImage": "İşlemdeki Görsel", + "noWorkflow": "İş Akışı Yok", + "newWorkflowDesc": "Yeni iş akışı?", + "downloadWorkflow": "İş Akışını İndir (JSON)", + "unknownErrorValidatingWorkflow": "İş akışını doğrulamada bilinmeyen bir sorun", + "unableToGetWorkflowVersion": "İş akışı sürümüne ulaşılamadı", + "newWorkflowDesc2": "Geçerli iş akışında kaydedilmemiş değişiklikler var.", + "cannotConnectInputToInput": "Giriş girişe bağlanamaz", + "zoomInNodes": "Yakınlaştır", + "boolean": "Boole Değeri", + "edge": "Uç", + "zoomOutNodes": "Uzaklaştır", + "cannotConnectOutputToOutput": "Çıkış çıkışa bağlanamaz", + "cannotConnectToSelf": "Kendisine bağlanamaz", + "cannotDuplicateConnection": "Kopya bağlantılar yaratılamaz" + }, + "workflows": { + "workflowName": "İş Akışı Adı", + "problemSavingWorkflow": "İş Akışını Kaydetmede Sorun", + "saveWorkflow": "İş Akışını Kaydet", + "uploadWorkflow": "Dosyadan Yükle", + "newWorkflowCreated": "Yeni İş Akışı Yaratıldı", + "loading": "İş Akışları Yükleniyor", + "workflowEditorMenu": "İş Akışı Düzenleyici Menüsü", + "downloadWorkflow": "İndir", + "saveWorkflowAs": "İş Akışını Farklı Kaydet", + "savingWorkflow": "İş Akışı Kaydediliyor...", + "workflows": "İş Akışları", + "workflowLibrary": "Depo", + "deleteWorkflow": "İş Akışını Sil", + "unnamedWorkflow": "Adsız İş Akışı", + "noWorkflows": "İş Akışı Yok", + "workflowSaved": "İş Akışı Kaydedildi" + }, + "toast": { + "problemRetrievingWorkflow": "İş Akışını Getirmede Sorun", + "workflowDeleted": "İş Akışı Silindi", + "loadedWithWarnings": "İş Akışı Yüklendi Ancak Uyarılar Var", + "workflowLoaded": "İş Akışı Yüklendi", + "problemDeletingWorkflow": "İş Akışını Silmede Sorun" + }, + "parameters": { + "invoke": { + "noPrompts": "İstem oluşturulmadı", + "noModelSelected": "Model seçilmedi", + "systemDisconnected": "Sistem bağlantısı kesildi", + "invoke": "Invoke" + }, + "clipSkip": "CLIP Atlama", + "cfgScale": "CFG Ölçeği", + "controlNetControlMode": "Yönetim Kipi", + "general": "Genel", + "seamlessYAxis": "Dikişsiz Döşeme Y Ekseni", + "maskBlur": "Bulandırma", + "images": "Görseller", + "info": "Bilgi", + "positivePromptPlaceholder": "Olumlu İstem", + "scaledHeight": "Ölçekli Boy", + "lockAspectRatio": "En-Boy Oranını Koru", + "swapDimensions": "Çevir", + "setToOptimalSize": "Modele göre en uygun boyut", + "copyImage": "Görseli Kopyala", + "height": "Boy", + "width": "En", + "useSize": "Boyutu Kullan", + "symmetry": "Bakışım", + "tileSize": "Döşeme Boyutu", + "strength": "Güç", + "useAll": "Hepsini Kullan", + "denoisingStrength": "Arındırma Ölçüsü", + "imageFit": "Öngörseli Çıktı Boyutuna Sığdır", + "noiseThreshold": "Gürültü Eşiği", + "seed": "Tohum", + "imageActions": "Görsel İşlemleri", + "shuffle": "Kar", + "usePrompt": "İstemi Kullan", + "setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (çok küçük olabilir)", + "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (çok büyük olabilir)", + "cfgRescaleMultiplier": "CFG Rescale Çarpanı", + "infillMethod": "Doldurma Yöntemi", + "steps": "Adım", + "upscaling": "Büyütülüyor", + "useSeed": "Tohumu Kullan", + "scheduler": "Planlayıcı", + "coherenceMode": "Kip", + "useCpuNoise": "CPU Gürültüsü Kullan", + "negativePromptPlaceholder": "Olumsuz İstem", + "patchmatchDownScaleSize": "Küçült", + "perlinNoise": "Perlin Gürültüsü", + "scaledWidth": "Ölçekli En", + "seamlessXAxis": "Dikişsiz Döşeme X Ekseni", + "type": "Tür" + }, + "modelManager": { + "baseModel": "Ana Model", + "active": "etkin", + "deleteConfig": "Yapılandırmayı Sil", + "availableModels": "Kullanılabilir Modeller", + "advanced": "Gelişmiş", + "allModels": "Tüm Modeller", + "alpha": "Alfa", + "config": "Yapılandırma", + "addModel": "Model Ekle", + "height": "Boy", + "modelDeleted": "Model Kaldırıldı", + "vaePrecision": "VAE Kesinliği", + "convertToDiffusersHelpText6": "Bu modeli dönüştürmek istiyor musunuz?", + "deleteMsg1": "Bu modeli InvokeAI'dan silmek istediğinize emin misiniz?", + "settings": "Seçenekler", + "vae": "VAE", + "width": "En", + "delete": "Sil", + "convert": "Dönüştür", + "syncModels": "Modelleri Senkronize Et", + "variant": "Tür", + "convertingModelBegin": "Model Dönüştürülüyor. Lütfen bekleyiniz.", + "none": "hiçbiri", + "search": "Ara", + "model": "Model", + "modelType": "Model Türü", + "modelUpdated": "Model Güncellendi", + "modelUpdateFailed": "Model Güncellenemedi", + "name": "Ad", + "selected": "Seçili", + "convertToDiffusersHelpText5": "Lütfen yeterli depolama alanınız olduğundan emin olun. Modeller çoğunlukla 2-7 GB boyutundadır.", + "modelManager": "Model Yöneticisi", + "convertToDiffusersHelpText4": "Bu işlem yalnızca bir kez yapılır, bilgisayarınızın özelliklerine bağlı olarak yaklaşık 30-60 saniye sürebilir.", + "deleteModel": "Modeli Sil", + "deleteMsg2": "Model InvokeAI ana klasöründeyse bilgisayarınızdan silinir, bu klasör dışındaysa bilgisayarınızdan silinmeyecektir.", + "load": "Yükle", + "modelDeleteFailed": "Model kaldırılamadı", + "noModelSelected": "Model Seçilmedi", + "predictionType": "Saptama Türü", + "selectModel": "Model Seç", + "modelConversionFailed": "Model Dönüşümü Başarısız", + "modelConverted": "Model Dönüştürüldü", + "description": "Tanım" + }, + "models": { + "addLora": "LoRA Ekle", + "defaultVAE": "Varsayılan VAE", + "lora": "LoRA", + "noModelsAvailable": "Model yok", + "noMatchingModels": "Uygun Model Yok", + "loading": "yükleniyor", + "selectModel": "Model Seçin" + }, + "settings": { + "generation": "Oluşturma" + }, + "sdxl": { + "cfgScale": "CFG Ölçeği", + "loading": "Yükleniyor...", + "denoisingStrength": "Arındırma Ölçüsü" + } +} diff --git a/invokeai/frontend/web/public/locales/uk.json b/invokeai/frontend/web/public/locales/uk.json new file mode 100644 index 00000000000..9d530068c2e --- /dev/null +++ b/invokeai/frontend/web/public/locales/uk.json @@ -0,0 +1,116 @@ +{ + "common": { + "hotkeysLabel": "Гарячi клавіші", + "languagePickerLabel": "Мова", + "reportBugLabel": "Повідомити про помилку", + "settingsLabel": "Налаштування", + "img2img": "Зображення із зображення (img2img)", + "nodes": "Вузли", + "upload": "Завантажити", + "load": "Завантажити", + "statusDisconnected": "Відключено", + "cancel": "Скасувати", + "accept": "Підтвердити", + "back": "Назад", + "postprocessing": "Постобробка", + "loading": "Завантаження", + "githubLabel": "Github", + "txt2img": "Текст в зображення (txt2img)", + "discordLabel": "Discord", + "linear": "Лінійна обробка" + }, + "gallery": { + "galleryImageSize": "Розмір зображень", + "gallerySettings": "Налаштування галереї", + "autoSwitchNewImages": "Автоматично вибирати нові" + }, + "modelManager": { + "modelManager": "Менеджер моделей", + "model": "Модель", + "modelUpdated": "Модель оновлена", + "manual": "Ручне", + "name": "Назва", + "description": "Опис", + "config": "Файл конфігурації", + "width": "Ширина", + "height": "Висота", + "addModel": "Додати модель", + "availableModels": "Доступні моделі", + "search": "Шукати", + "load": "Завантажити", + "active": "активна", + "selected": "Обрані", + "delete": "Видалити", + "deleteModel": "Видалити модель", + "deleteConfig": "Видалити конфігурацію", + "deleteMsg1": "Ви точно хочете видалити модель із InvokeAI?", + "deleteMsg2": "Це не призведе до видалення файлу моделі з диску. Позніше ви можете додати його знову.", + "allModels": "Усі моделі", + "convert": "Конвертувати", + "convertToDiffusers": "Конвертувати в Diffusers", + "convertToDiffusersHelpText3": "Файл моделі на диску НЕ буде видалено або змінено. Ви можете знову додати його в Model Manager, якщо потрібно.", + "alpha": "Альфа", + "repo_id": "ID репозиторію", + "convertToDiffusersHelpText5": "Переконайтеся, що у вас достатньо місця на диску. Моделі зазвичай займають від 4 до 7 Гб.", + "convertToDiffusersHelpText6": "Ви хочете перетворити цю модель?", + "modelConverted": "Модель перетворено", + "none": "пусто", + "convertToDiffusersHelpText4": "Це одноразова дія. Вона може зайняти від 30 до 60 секунд в залежності від характеристик вашого комп'ютера.", + "convertToDiffusersHelpText1": "Ця модель буде конвертована в формат 🧨 Diffusers.", + "convertToDiffusersHelpText2": "Цей процес замінить ваш запис в Model Manager на версію тієї ж моделі в Diffusers." + }, + "parameters": { + "images": "Зображення", + "steps": "Кроки", + "cfgScale": "Рівень CFG", + "width": "Ширина", + "height": "Висота", + "seed": "Сід", + "shuffle": "Оновити", + "noiseThreshold": "Поріг шуму", + "perlinNoise": "Шум Перліна", + "type": "Тип", + "strength": "Сила", + "upscaling": "Збільшення", + "scale": "Масштаб", + "imageFit": "Вмістити зображення", + "scaleBeforeProcessing": "Масштабувати", + "scaledWidth": "Масштаб Ш", + "scaledHeight": "Масштаб В", + "infillMethod": "Засіб заповнення", + "tileSize": "Розмір області", + "usePrompt": "Використати запит", + "useSeed": "Використати сід", + "useAll": "Використати все", + "info": "Метадані", + "general": "Основне", + "denoisingStrength": "Сила шумоподавлення", + "copyImage": "Копіювати зображення", + "symmetry": "Симетрія" + }, + "settings": { + "models": "Моделі", + "displayInProgress": "Показувати процес генерації", + "confirmOnDelete": "Підтверджувати видалення", + "resetWebUI": "Повернути початкові", + "resetWebUIDesc1": "Скидання настройок веб-інтерфейсу видаляє лише локальний кеш браузера з вашими зображеннями та налаштуваннями. Це не призводить до видалення зображень з диску.", + "resetWebUIDesc2": "Якщо зображення не відображаються в галереї або не працює ще щось, спробуйте скинути налаштування, перш ніж повідомляти про проблему на GitHub.", + "resetComplete": "Інтерфейс скинуто. Оновіть цю сторінку." + }, + "toast": { + "uploadFailed": "Не вдалося завантажити", + "imageCopied": "Зображення скопійоване", + "parametersNotSet": "Параметри не задані", + "serverError": "Помилка сервера", + "connected": "Підключено до сервера", + "canceled": "Обробку скасовано" + }, + "accessibility": { + "nextImage": "Наступне зображення", + "invokeProgressBar": "Індикатор виконання", + "reset": "Скинути", + "uploadImage": "Завантажити зображення", + "previousImage": "Попереднє зображення", + "menu": "Меню" + } +} diff --git a/invokeai/frontend/web/public/locales/vi.json b/invokeai/frontend/web/public/locales/vi.json new file mode 100644 index 00000000000..929cf2cbf79 --- /dev/null +++ b/invokeai/frontend/web/public/locales/vi.json @@ -0,0 +1,2743 @@ +{ + "accessibility": { + "uploadImages": "Tải Lên Hình Ảnh", + "previousImage": "Ảnh trước đó", + "about": "Giới Thiệu", + "nextImage": "Ảnh tiếp theo", + "reset": "Khởi Động Lại", + "toggleRightPanel": "Bật/Tắt Bảng Bên Phải (G)", + "toggleLeftPanel": "Bật/Tắt Bảng Bên Trái (T)", + "menu": "Menu", + "createIssue": "Mở Vấn Đề", + "resetUI": "$t(accessibility.reset) Giao Diện Người Dùng", + "mode": "Chế Độ", + "invokeProgressBar": "Thanh Tiến Trình", + "submitSupportTicket": "Gửi Phiếu Hỗ Trợ", + "uploadImage": "Tải Lên Hình Ảnh" + }, + "boards": { + "autoAddBoard": "Tự Động Thêm Bảng", + "addBoard": "Thêm Bảng", + "downloadBoard": "Tải Xuống Bảng", + "movingImagesToBoard_other": "Di chuyển {{count}} ảnh vào Bảng:", + "noBoards": "Không Có Bảng Thuộc Loại {{boardType}}", + "noMatching": "Không Có Bảng Tương Ứng", + "searchBoard": "Tìm Bảng...", + "addPrivateBoard": "Thêm Bảng Cá Nhân", + "addSharedBoard": "Thêm Bảng Nhóm", + "boards": "Bảng", + "selectedForAutoAdd": "Đã Chọn Để Tự động thêm", + "myBoard": "Bảng Của Tôi", + "deletedPrivateBoardsCannotbeRestored": "Bảng và ảnh đã xoá sẽ không thể khôi phục lại. Chọn 'Chỉ Xoá Bảng' sẽ dời ảnh vào trạng thái chưa phân loại riêng cho chủ ảnh.", + "changeBoard": "Thay Đổi Bảng", + "clearSearch": "Làm Sạch Thanh Tìm Kiếm", + "updateBoardError": "Lỗi khi cập nhật Bảng", + "private": "Bảng Cá Nhân", + "shared": "Bảng Nhóm", + "imagesWithCount_other": "{{count}} hình ảnh", + "cancel": "Huỷ", + "deleteBoard": "Xoá Bảng", + "deleteBoardAndImages": "Xoá Bảng Lẫn Hình ảnh", + "deleteBoardOnly": "Chỉ Xoá Bảng", + "deletedBoardsCannotbeRestored": "Bảng và ảnh đã xoá sẽ không thể khôi phục lại. Chọn 'Chỉ Xoá Bảng' sẽ dời ảnh vào trạng thái chưa phân loại.", + "bottomMessage": "Việc xóa ảnh sẽ khởi động lại mọi tính năng đang sử dụng chúng.", + "menuItemAutoAdd": "Tự động thêm cho Bảng này", + "move": "Di Chuyển", + "topMessage": "Lựa chọn này chứa ảnh được dùng với những tính năng sau:", + "uncategorized": "Chưa Sắp Xếp", + "archived": "Được Lưu Trữ", + "loading": "Đang Tải...", + "selectBoard": "Chọn Bảng", + "archiveBoard": "Lưu trữ Bảng", + "unarchiveBoard": "Ngừng Lưu Trữ Bảng", + "assetsWithCount_other": "{{count}} tài nguyên", + "uncategorizedImages": "Ảnh Chưa Sắp Xếp", + "deleteAllUncategorizedImages": "Xoá Tất Cả Ảnh Chưa Sắp Xếp", + "locateInGalery": "Vị Trí Ở Thư Viện Ảnh", + "deletedImagesCannotBeRestored": "Ảnh đã xóa không thể khôi phục lại.", + "hideBoards": "Ẩn Bảng", + "viewBoards": "Xem Bảng" + }, + "gallery": { + "swapImages": "Đổi Hình Ảnh", + "dropToUpload": "$t(gallery.drop) Để Tải Lên", + "deleteSelection": "Xoá Phần Được Lựa Chọn", + "hover": "Di Chuột", + "deleteImage_other": "Xoá {{count}} Hình Ảnh", + "compareImage": "So Sánh Ảnh", + "compareHelp4": "Nhấn Z hoặc Esc để thoát.", + "compareHelp3": "Nhấn C để đổi ảnh được so sánh.", + "compareHelp1": "Giữ Alt khi bấm vào ảnh trong thư viện ảnh hoặc dùng phím mũi tên để đổi ảnh dùng cho so sánh.", + "showArchivedBoards": "Hiển Thị Bảng Được Lưu Trữ", + "drop": "Thả", + "copy": "Sao Chép", + "selectAllOnPage": "Chọn Tất Cả Trên Trang", + "bulkDownloadFailed": "Tải Xuống Thất Bại", + "bulkDownloadRequestFailed": "Có Vấn Đề Khi Đang Chuẩn Bị Tải Xuống", + "download": "Tải Xuống", + "dropOrUpload": "Kéo Thả Hoặc Tải Lên", + "currentlyInUse": "Hình ảnh này hiện đang sử dụng các tính năng sau:", + "deleteImagePermanent": "Ảnh đã xoá không thể phục hồi.", + "exitSearch": "Thoát Tìm Kiếm Hình Ảnh", + "exitBoardSearch": "Thoát Tìm Kiểm Bảng", + "gallery": "Thư Viện Ảnh", + "galleryImageSize": "Kích Thước Ảnh", + "downloadSelection": "Tải xuống Phần Được Lựa Chọn", + "bulkDownloadRequested": "Chuẩn Bị Tải Xuống", + "newestFirst": "Mới Nhất Trước", + "showStarredImagesFirst": "Hiển Thị Ảnh Gắn Sao Trước", + "bulkDownloadRequestedDesc": "Yêu cầu tải xuống đang được chuẩn bị. Vui lòng chờ trong giây lát.", + "starImage": "Gắn Sao", + "viewerImage": "Trình Xem Ảnh", + "sideBySide": "Cạnh Nhau", + "alwaysShowImageSizeBadge": "Luôn Hiển Thị Kích Thước Ảnh", + "autoAssignBoardOnClick": "Tự Động Gán Vào Bảng Khi Nhấp Chuột", + "go": "Đi", + "autoSwitchNewImages": "Tự Động Đổi Sang Hình Ảnh Mới", + "featuresWillReset": "Nếu bạn xoá hình ảnh này, những tính năng đó sẽ lập tức được khởi động lại.", + "openInViewer": "Mở Trong Trình Xem", + "searchImages": "Tìm Theo Metadata", + "selectForCompare": "Chọn Để So Sánh", + "move": "Di Chuyển", + "displayBoardSearch": "Tìm Kiếm Bảng", + "displaySearch": "Tìm Kiếm Hình Ảnh", + "slider": "Thanh Trượt", + "gallerySettings": "Cài Đặt Thư Viện Ảnh", + "image": "hình ảnh", + "noImageSelected": "Không Có Ảnh Được Chọn", + "assetsTab": "Tài liệu bạn đã tải lên để dùng cho dự án của mình.", + "imagesTab": "Ảnh bạn vừa được tạo và lưu trong Invoke.", + "loading": "Đang Tải", + "oldestFirst": "Cũ Nhất Trước", + "exitCompare": "Ngừng So Sánh", + "stretchToFit": "Kéo Dài Cho Vừa Vặn", + "sortDirection": "Cách Sắp Xếp", + "unstarImage": "Bỏ Gắn Sao", + "compareHelp2": "Nhấn M để tuần hoàn trong chế độ so sánh.", + "boardsSettings": "Thiết Lập Bảng", + "imagesSettings": "Cài Đặt Ảnh Trong Thư Viện Ảnh", + "assets": "Tài Nguyên", + "images": "Hình Ảnh", + "useForPromptGeneration": "Dùng Để Tạo Sinh Lệnh", + "jump": "Nhảy Đến", + "noImagesInGallery": "Không Có Ảnh Để Hiển Thị", + "unableToLoad": "Không Thể Tải Thư Viện Ảnh", + "selectAnImageToCompare": "Chọn Ảnh Để So Sánh", + "openViewer": "Mở Trình Xem", + "closeViewer": "Đóng Trình Xem" + }, + "common": { + "ipAdapter": "IP Adapter", + "positivePrompt": "Lệnh Tích Cực", + "negativePrompt": "Lệnh Tiêu Cực", + "editor": "Biên Tập Viên", + "loading": "Đang Tải", + "clipboard": "Clipboard", + "learnMore": "Tìm Hiểu Thêm", + "openInViewer": "Mở Trong Trình Xem", + "alpha": "Alpha", + "edit": "Sửa", + "nodes": "Workflow", + "format": "Định Dạng", + "delete": "Xoá", + "details": "Chi Tiết", + "img2img": "Hình ảnh sang Hình ảnh", + "upload": "Tải Lên", + "somethingWentWrong": "Có vấn đề phát sinh", + "statusDisconnected": "Mất Kết Nối", + "t2iAdapter": "T2I Adapter", + "orderBy": "Sắp Xếp Theo", + "random": "Ngẫu Nhiên", + "settingsLabel": "Cài Đặt", + "reportBugLabel": "Báo Lỗi", + "controlNet": "ControlNet", + "apply": "Áp Dụng", + "view": "Xem", + "dontAskMeAgain": "Không hỏi lại", + "error": "Lỗi", + "or": "hoặc", + "installed": "Được Tải Xuống Sẵn", + "simple": "Cơ Bản", + "linear": "Tuyến Tính", + "safetensors": "Safetensors", + "off": "Tắt", + "add": "Thêm", + "load": "Tải", + "accept": "Đồng Ý", + "communityLabel": "Cộng Đồng", + "discordLabel": "Discord", + "back": "Trở Về", + "advanced": "Nâng Cao", + "batch": "Quản Lý Lô", + "modelManager": "Quản Lý Model", + "dontShowMeThese": "Không hiển thị thứ này", + "ok": "OK", + "placeholderSelectAModel": "Chọn một model", + "reset": "Khởi Động Lại", + "none": "Không Có", + "on": "Bật", + "checkpoint": "Checkpoint", + "txt2img": "Từ Ngữ Sang Hình Ảnh", + "unknown": "Không Rõ", + "githubLabel": "Github", + "folder": "Thư mục", + "hotkeysLabel": "Phím Tắt", + "loadingImage": "Đang Tải Hình ảnh", + "input": "Đầu Vào", + "languagePickerLabel": "Ngôn Ngữ", + "openInNewTab": "Mở Trong Tab Mới", + "outpaint": "outpaint", + "save": "Lưu", + "saveAs": "Lưu Như", + "auto": "Tự Động", + "inpaint": "inpaint", + "beta": "Beta", + "toResolve": "Để khắc phục", + "areYouSure": "Bạn chắc chứ?", + "ai": "ai", + "aboutDesc": "Sử dụng Invoke cho công việc? Xem thử:", + "aboutHeading": "Quyền Năng Sáng Tạo Của Riêng", + "enabled": "Đã Bật", + "close": "Đóng", + "data": "Dữ Liệu", + "file": "Tài liệu", + "outputs": "Đầu Ra", + "postprocessing": "Xử Lý Hậu Kỳ", + "template": "Mẫu Trình Bày", + "copy": "Sao Chép", + "copyError": "Lỗi Khi $t(gallery.copy)", + "updated": "Đã Cập Nhật", + "created": "Đã Tạo", + "red": "Đỏ", + "disabled": "Đã Tắt", + "new": "Mới", + "blue": "Lam", + "green": "Lục", + "cancel": "Huỷ", + "direction": "Phương Hướng", + "unknownError": "Lỗi Không Rõ", + "selected": "Đã chọn", + "tab": "Tab", + "loadingModel": "Đang Tải Model", + "generating": "Đang Tạo Sinh", + "warnings": "Cảnh Báo", + "count": "Đếm", + "step": "Bước", + "values": "Giá Trị", + "start": "Bắt Đầu", + "end": "Kết Thúc", + "min": "Tối Thiểu", + "max": "Tối Đa", + "seed": "Hạt Giống", + "combinatorial": "Tổ Hợp", + "column": "Cột", + "layout": "Bố Cục", + "row": "Hàng", + "board": "Bảng", + "saveChanges": "Lưu Thay Đổi", + "error_withCount_other": "{{count}} lỗi", + "value": "Giá Trị", + "label": "Nhãn Tên", + "systemInformation": "Thông Tin Hệ Thống", + "model_withCount_other": "{{count}} model", + "noOptions": "Không Có Lựa Chọn", + "noMatches": "Không Có Mục Phù Hợp", + "search": "Tìm Kiếm", + "clear": "Dọn Dẹp", + "compactView": "Chế Độ Xem Gọn", + "fullView": "Chế Độ Xem Đầy Đủ", + "options_withCount_other": "{{count}} thiết lập", + "removeNegativePrompt": "Xóa Lệnh Tiêu Cực", + "addNegativePrompt": "Thêm Lệnh Tiêu Cực", + "selectYourModel": "Chọn Model", + "goTo": "Đi Đến", + "imageFailedToLoad": "Không Thể Tải Ảnh", + "localSystem": "Hệ Thống Máy Chủ", + "notInstalled": "Chưa $t(common.installed)", + "prevPage": "Trang Trước", + "nextPage": "Trang Sau", + "resetToDefaults": "Tải Lại Mặc Định" + }, + "prompt": { + "addPromptTrigger": "Thêm Trigger Cho Lệnh", + "compatibleEmbeddings": "Embedding Tương Thích", + "noMatchingTriggers": "Không có trigger phù hợp", + "generateFromImage": "Tạo sinh lệnh từ ảnh", + "expandCurrentPrompt": "Mở Rộng Lệnh Hiện Tại", + "uploadImageForPromptGeneration": "Tải Ảnh Để Tạo Sinh Lệnh", + "expandingPrompt": "Đang mở rộng lệnh...", + "replace": "Thay Thế", + "discard": "Huỷ Bỏ", + "resultTitle": "Mở Rộng Lệnh Hoàn Tất", + "resultSubtitle": "Chọn phương thức mở rộng lệnh:", + "insert": "Chèn" + }, + "queue": { + "resume": "Tiếp Tục", + "enqueueing": "Xếp Vào Hàng Hàng Loạt", + "prompts_other": "Lệnh", + "iterations_other": "Vòng Lặp", + "total": "Tổng", + "pruneFailed": "Có Vấn Đề Khi Cắt Bớt Mục Khỏi Hàng", + "clearSucceeded": "Hàng Đã Được Dọn Sạch", + "cancel": "Huỷ Bỏ", + "clearQueueAlertDialog2": "Bạn chắc chắn muốn dọn sạch hàng không?", + "queueEmpty": "Hàng Trống", + "queueBack": "Thêm Vào Hàng", + "openQueue": "Mở Queue", + "pause": "Dừng Lại", + "pauseFailed": "Có Vấn Đề Khi Dừng Lại Bộ Xử Lý", + "batchQueued": "Lô Đã Vào Hàng", + "batchFailedToQueue": "Lỗi Khi Xếp Lô Vào Hàng", + "next": "Tiếp Theo", + "in_progress": "Đang Chạy", + "failed": "Thất Bại", + "canceled": "Bị Huỷ", + "cancelBatchFailed": "Có Vấn Đề Khi Huỷ Bỏ Lô", + "workflows": "Workflow (Luồng làm việc)", + "canvas": "Canvas (Vùng ảnh)", + "upscaling": "Upscale (Nâng Cấp Chất Lượng Hình Ảnh)", + "generation": "Generation (Máy Tạo sinh)", + "back": "sau", + "pruneTooltip": "Cắt bớt {{item_count}} mục đã hoàn tất", + "pruneSucceeded": "Đã cắt bớt {{item_count}} mục đã hoàn tất khỏi hàng", + "clearTooltip": "Huỷ Và Dọn Dẹp Tất Cả Mục", + "clearQueueAlertDialog": "Dọn dẹp hàng đợi sẽ ngay lập tức huỷ tất cả mục đang xử lý và làm sạch hàng hoàn toàn. Bộ lọc đang chờ xử lý sẽ bị huỷ bỏ và Vùng Dựng Canva sẽ được khởi động lại.", + "session": "Phiên", + "item": "Mục", + "resumeFailed": "Có Vấn Đề Khi Tiếp Tục Bộ Xử Lý", + "resumeSucceeded": "Bộ Xử Lý Đã Tiếp Tục", + "cancelTooltip": "Huỷ Bỏ Mục Hiện Tại", + "cancelFailed": "Có Vấn Đề Khi Huỷ Bỏ Mục Hiện Tại", + "prune": "Cắt Bớt", + "clear": "Dọn Dẹp", + "queue": "Queue (Hàng Đợi)", + "queueFront": "Thêm Vào Đầu Hàng", + "resumeTooltip": "Tiếp Tục Bộ Xử Lý", + "clearFailed": "Có Vấn Đề Khi Dọn Dẹp Hàng", + "generations_other": "Ảnh Tạo Sinh", + "cancelBatch": "Huỷ Bỏ Lô", + "status": "Trạng Thái", + "pending": "Đang Chờ", + "gallery": "Thư Viện Ảnh", + "front": "trước", + "batch": "Lô", + "origin": "Nguồn Gốc", + "destination": "Điểm Đến", + "other": "Khác", + "graphFailedToQueue": "Lỗi Khi Xếp Đồ Thị Vào Hàng", + "notReady": "Không Thể Xếp Hàng", + "cancelItem": "Huỷ Bỏ Mục", + "cancelBatchSucceeded": "Lô Đã Huỷ Bỏ", + "current": "Hiện Tại", + "time": "Thời Gian", + "completed": "Hoàn Tất", + "pauseTooltip": "Dừng Lại Bộ Xử Lý", + "pauseSucceeded": "Bộ Xử Lý Đã Dừng Lại", + "cancelSucceeded": "Mục Đã Huỷ Bỏ", + "completedIn": "Hoàn tất trong", + "graphQueued": "Đồ Thị Đã Vào Hàng", + "batchQueuedDesc_other": "Thêm {{count}} phiên vào {{direction}} của hàng", + "batchSize": "Kích Thước Lô", + "cancelAllExceptCurrentQueueItemAlertDialog": "Huỷ tất cả mục đang xếp hàng ngoại trừ việc nó sẽ dừng các mục đang chờ nhưng cho phép các mục đang chạy được hoàn tất.", + "cancelAllExceptCurrentQueueItemAlertDialog2": "Bạn có chắc muốn huỷ tất cả mục đang chờ?", + "cancelAllExceptCurrentTooltip": "Huỷ Bỏ Tất Cả Ngoại Trừ Mục Hiện Tại", + "confirm": "Đồng Ý", + "retrySucceeded": "Mục Đã Thử Lại", + "retryFailed": "Có Vấn Đề Khi Thử Lại Mục", + "retryItem": "Thử Lại Mục", + "credits": "Nguồn", + "cancelAllExceptCurrent": "Huỷ Bỏ Tất Cả Ngoại Trừ Mục Hiện Tại", + "createdAt": "Tạo tại", + "completedAt": "Hoàn Thành Tại", + "sortColumn": "Sắp Xếp Cột", + "sortBy": "Sắp Xếp Theo {{column}}", + "sortOrderAscending": "Tăng Dần", + "sortOrderDescending": "Giảm Dần" + }, + "hotkeys": { + "canvas": { + "fitLayersToCanvas": { + "title": "Xếp Vừa Layers Vào Canvas", + "desc": "Căn chỉnh để góc nhìn vừa vặn với tất cả layer nhìn thấy dược." + }, + "setZoomTo800Percent": { + "desc": "Phóng to canvas lên 800%.", + "title": "Phóng To Vào 800%" + }, + "transformSelected": { + "title": "Biến Đổi", + "desc": "Biến đổi layer được chọn." + }, + "fitBboxToCanvas": { + "title": "Xếp Vừa Hộp Giới Hạn Vào Canvas", + "desc": "Căn chỉnh để góc nhìn vừa vặn với hộp giới hạn." + }, + "setZoomTo400Percent": { + "desc": "Phóng to canvas lên 400%.", + "title": "Phóng To Vào 400%" + }, + "decrementToolWidth": { + "desc": "Giảm độ rộng của cọ hoặc tẩy, tuỳ theo cái được chọn.", + "title": "Giảm Độ Rộng" + }, + "setZoomTo100Percent": { + "desc": "Phóng to canvas lên 100%.", + "title": "Phóng To Vào 100%" + }, + "setZoomTo200Percent": { + "title": "Phóng To Vào 200%", + "desc": "Phóng to canvas lên 200%." + }, + "prevEntity": { + "desc": "Chọn layer trước đó trong danh sách.", + "title": "Layer Trước Đó" + }, + "redo": { + "title": "Làm Lại", + "desc": "Khôi phục hành động cuối cùng lên canvas sau khi bị hoàn tác." + }, + "nextEntity": { + "title": "Layer Tiếp Theo", + "desc": "Chọn layer tiếp theo trong danh sách." + }, + "selectBrushTool": { + "title": "Cọ", + "desc": "Dùng cọ." + }, + "selectBboxTool": { + "desc": "Dùng hộp giới hạn.", + "title": "Hộp Giới Hạn" + }, + "incrementToolWidth": { + "title": "Tăng Độ Rộng", + "desc": "Tăng độ rộng của cọ hoặc tẩy, tuỳ theo cái được chọn." + }, + "selectEraserTool": { + "title": "Tẩy", + "desc": "Dùng tẩy." + }, + "title": "Canvas (Vùng Ảnh)", + "selectColorPickerTool": { + "title": "Chọn Màu", + "desc": "Dùng công cụ chọn màu." + }, + "selectViewTool": { + "title": "Xem", + "desc": "Dùng công cụ xem." + }, + "selectRectTool": { + "desc": "Dùng công cụ vẽ hình chữ nhật.", + "title": "Hình Chữ Nhật" + }, + "selectMoveTool": { + "title": "Di Chuyển", + "desc": "Dùng công cụ di chuyển." + }, + "deleteSelected": { + "desc": "Xoá layer được chọn.", + "title": "Xoá Layer" + }, + "quickSwitch": { + "title": "Đổi Layer Nhanh", + "desc": "Đổi giữa hai layer cuối cùng được chọn. Nếu một layer bị đánh dấu, luôn luôn đổi giữa nó với layer bị đánh dấu cuối cùng." + }, + "undo": { + "title": "Hoàn Tác", + "desc": "Hoàn tác hành động cuối cùng lên canvas." + }, + "applyTransform": { + "desc": "Áp dụng lệnh biến đổi đang chờ sẵn cho layer được chọn.", + "title": "Áp Dụng Lệnh Biến Đổi" + }, + "cancelFilter": { + "title": "Huỷ Bộ Lọc", + "desc": "Huỷ bộ lọc đang chờ sẵn." + }, + "cancelTransform": { + "title": "Huỷ Lệnh Biến Đổi", + "desc": "Huỷ lệnh biến đổi đang chờ sẵn cho layer được chọn." + }, + "resetSelected": { + "title": "Làm Mới Layer", + "desc": "Làm mới lại layer được chọn. Chỉ áp dụng cho Lớp Phủ Inpaint và Chỉ Dẫn Khu Vực." + }, + "filterSelected": { + "title": "Bộ Lọc", + "desc": "Lọc layer được lựa chọn. Chỉ áp dụng cho layer dạng Raster và layer điều khiển được." + }, + "applyFilter": { + "title": "Áp Dụng Bộ Lộc", + "desc": "Áp dụng bộ lọc đang chờ sẵn cho layer được chọn." + }, + "settings": { + "behavior": "Hành Vi", + "display": "Hiển Thị", + "grid": "Lưới", + "debug": "Gỡ Lỗi" + }, + "toggleNonRasterLayers": { + "title": "Bật/Tắt Layer Không Thuộc Dạng Raster", + "desc": "Hiện hoặc ẩn tất cả layer không thuộc dạng raster (Layer Điều Khiển Được, Lớp Phủ Inpaint, Chỉ Dẫn Khu Vực)." + }, + "invertMask": { + "title": "Đảo Ngược Lớp Phủ", + "desc": "Đảo ngược lớp phủ inpaint được chọn, tạo một lớp phủ mới với độ trong suốt đối nghịch." + }, + "fitBboxToMasks": { + "title": "Xếp Vừa Hộp Giới Hạn Vào Lớp Phủ", + "desc": "Tự động điểu chỉnh hộp giới hạn tạo sinh vừa vặn vào lớp phủ inpaint nhìn thấy được" + }, + "applySegmentAnything": { + "title": "Áp Dụng Segment Anything", + "desc": "Áp dụng lớp phủ Segment Anything hiện tại.", + "key": "enter" + }, + "cancelSegmentAnything": { + "title": "Huỷ Segment Anything", + "desc": "Huỷ hoạt động Segment Anything hiện tại.", + "key": "esc" + }, + "fitBboxToLayers": { + "title": "Xếp Vừa Hộp Giới Hạn Vào Layer", + "desc": "Tự động điểu chỉnh hộp giới hạn tạo sinh vừa vặn vào layer nhìn thấy được" + }, + "toggleBbox": { + "title": "Bật/Tắt Hiển Thị Hộp Giới Hạn", + "desc": "Ẩn hoặc hiện hộp giới hạn tạo sinh" + }, + "setFillColorsToDefault": { + "title": "Đặt Màu Lại Mặc Định", + "desc": "Chỉnh công cụ màu hiện tại về mặc định." + }, + "toggleFillColor": { + "title": "Bật/Tắt Màu Lấp Đầy", + "desc": "Bật/Tắt công cụ đổ màu hiện tại." + } + }, + "workflows": { + "title": "Workflow (Luồng Làm Việc)", + "pasteSelection": { + "desc": "Dán node và kết nối đã chọn.", + "title": "Dán" + }, + "pasteSelectionWithEdges": { + "title": "Dán Với Các Kết Nối", + "desc": "Dán tất cả node, kết nối và toàn bộ kết nối liên kết với node được sao chép." + }, + "copySelection": { + "title": "Sao Chép", + "desc": "Sao chép node và kết nối đã chọn." + }, + "deleteSelection": { + "title": "Xoá", + "desc": "Xoá node và kết nối." + }, + "redo": { + "title": "Làm Lại", + "desc": "Khôi phục hành động cuối cùng lên workflow được hoàn tác." + }, + "addNode": { + "desc": "Mở menu thêm node.", + "title": "Thêm Node" + }, + "selectAll": { + "title": "Chọn Tất Cả", + "desc": "Chọn tất cả node và kết nối." + }, + "undo": { + "desc": "Hoàn tác hành động cuối cùng lên workflow.", + "title": "Hoàn Tác" + } + }, + "viewer": { + "recallAll": { + "desc": "Gợi lại tất cả metadata của ảnh hiện tại.", + "title": "Gợi Lại Tất Cả Metadata" + }, + "recallSeed": { + "title": "Gợi Lại Hạt Giống", + "desc": "Gợi lại hạt giống của ảnh hiện tại." + }, + "useSize": { + "title": "Dùng Kích Thước", + "desc": "Dùng kích thước của ảnh hiện tại cho kích thước của hộp giới hạn." + }, + "toggleMetadata": { + "desc": "Hiển thị hoặc ẩn lớp phủ từ metadata của ảnh hiện tại.", + "title": "Hiển Thị/Ẩn Metadata" + }, + "title": "Trình Xem Ảnh", + "toggleViewer": { + "title": "Hiển Thị/Ẩn Trình Xem Ảnh", + "desc": "Hiển thị hoặc ẩn trình xem ảnh. Chỉ có trên tab Canvas." + }, + "recallPrompts": { + "title": "Gợi Lại Lệnh", + "desc": "Gợi lại lệnh tích cực lẫn tiêu cực của ảnh hiện tại." + }, + "loadWorkflow": { + "title": "Tải Từ Workflow", + "desc": "Tải hình ảnh hiện tại được lưu trong workflow (nếu có)." + }, + "nextComparisonMode": { + "title": "Chế Độ So Sánh Kế Tiếp", + "desc": "Tuần hoàn trong chế độ so sánh." + }, + "swapImages": { + "desc": "Đổi ảnh được so sánh.", + "title": "Đổi Ảnh So Sánh" + }, + "remix": { + "desc": "Gợi lại tất cả metadata cho hạt giống của ảnh hiện tại.", + "title": "Phối Lại" + }, + "runPostprocessing": { + "title": "Chạy Trình Xử Lý Hậu Kỳ", + "desc": "Chạy trình xử lý hậu kỳ được chọn cho anh hiện tại." + } + }, + "gallery": { + "galleryNavRight": { + "desc": "Sang phải theo mạng lưới thư viện ảnh, chọn hình ảnh đó. Nếu đến cuối hàng, qua hàng tiếp theo. Nếu đến hình ảnh cuối cùng, qua trang tiếp theo.", + "title": "Sang Phải" + }, + "galleryNavDown": { + "title": "Đi Xuống", + "desc": "Đi xuống theo mạng lưới thư viện ảnh, chọn hình ảnh đó. Nếu xuống cuối cùng trang, sang trang tiếp theo." + }, + "galleryNavLeft": { + "title": "Sang Trái", + "desc": "Sang trái theo mạng lưới thư viện ảnh, chọn hình ảnh đó. Nếu đến đầu hàng, về lại hàng trước đó. Nếu đến hình ảnh đầu tiên, về lại trang trước đó." + }, + "galleryNavUpAlt": { + "title": "Đi Lên (So Sánh Ảnh)", + "desc": "Giống với \"Đi Lên\", nhưng là chọn ảnh được so sánh, mở chế độ so sánh nếu chưa được mở." + }, + "deleteSelection": { + "desc": "Xoá ảnh được chọn. Theo mặc định, bạn sẽ được nhắc để chấp nhận thực hiện xoá. Nếu ảnh đang được dùng trong ứng dụng, bạn sẽ được cảnh báo.", + "title": "Xoá" + }, + "galleryNavUp": { + "title": "Đi Lên", + "desc": "Đi lên theo mạng lưới thư viện ảnh, chọn hình ảnh đó. Nếu lên trên cùng trang, về lại trang trước đó." + }, + "galleryNavRightAlt": { + "title": "Sang Phải (So Sánh Ảnh)", + "desc": "Giống với \"Sang Phải\", nhưng là chọn ảnh được so sánh, mở chế độ so sánh nếu chưa được mở." + }, + "selectAllOnPage": { + "title": "Chọn Tất Cả Trên Trang", + "desc": "Chọn tất cả ảnh trên trang hiện tại." + }, + "title": "Thư Viện Ảnh", + "galleryNavDownAlt": { + "title": "Đi Xuống (So Sánh Ảnh)", + "desc": "Giống với \"Đi Xuống\", nhưng là chọn ảnh được so sánh, mở chế độ so sánh nếu chưa được mở." + }, + "galleryNavLeftAlt": { + "desc": "Giống với \"Sang Trái\", nhưng là chọn ảnh được so sánh, mở chế độ so sánh nếu chưa được mở.", + "title": "Sang Trái (So Sánh Ảnh)" + }, + "clearSelection": { + "desc": "Xoá phần lựa chọn hiện tại nếu có.", + "title": "Xoá Phần Lựa Chọn" + }, + "starImage": { + "title": "Dấu/Huỷ Sao Hình Ảnh", + "desc": "Đánh dấu sao hoặc huỷ đánh dấu sao ảnh được chọn." + } + }, + "app": { + "togglePanels": { + "title": "Bật/Tắt Bảng", + "desc": "Hiển thị hoặc ẩn phần bảng bên trái và phải cùng lúc." + }, + "focusPrompt": { + "desc": "Đưa con trỏ chuột vào vùng lệnh tích cực.", + "title": "Chuyển Tập Trung Vào Lệnh" + }, + "toggleLeftPanel": { + "desc": "Hiển thị hoặc ẩn phần bảng bên trái.", + "title": "Bật/Tắt Bảng Bên Trái" + }, + "toggleRightPanel": { + "desc": "Hiển thị hoặc ẩn phần bảng bên phải.", + "title": "Bật/Tắt Bảng Bên Phải" + }, + "resetPanelLayout": { + "title": "Khởi Động Lại Cách Trình Bày Của Bảng", + "desc": "Khởi động lại phần bảng bên trái và phải vào kích thước và cách trình bày ban đầu." + }, + "selectQueueTab": { + "desc": "Chọn tab Queue (Hàng Đợi).", + "title": "Chọn Tab Queue" + }, + "invoke": { + "desc": "Xếp một đợt tạo sinh vào cuối hàng.", + "title": "Kích Hoạt" + }, + "invokeFront": { + "desc": "Xếp một đợt tạo sinh vào đầu hàng.", + "title": "Kích Hoạt (Đằng Trước)" + }, + "cancelQueueItem": { + "desc": "Huỷ bỏ mục hiện đang xếp hàng xử lý.", + "title": "Huỷ Bỏ" + }, + "clearQueue": { + "desc": "Huỷ và dọn sạch các mục đang xếp hàng.", + "title": "Dọn Sạch Hàng Đợi" + }, + "selectCanvasTab": { + "desc": "Chọn tab Canvas (Vùng ảnh).", + "title": "Chọn Tab Canvas" + }, + "title": "Ứng Dụng", + "selectUpscalingTab": { + "title": "Chọn Tab Upscale", + "desc": "Chọn tab Upscale (Nâng cấp chất lượng Hình ảnh)." + }, + "selectWorkflowsTab": { + "title": "Chọn Tab Workflow", + "desc": "Chọn tab Workflow (Luồng làm việc)." + }, + "selectModelsTab": { + "desc": "Chọn tab Model (Mô Hình).", + "title": "Chọn Tab Model" + }, + "selectGenerateTab": { + "title": "Chọn Tab Tạo Sinh", + "desc": "Chọn tab Tạo Sinh.", + "key": "1" + } + }, + "searchHotkeys": "Tìm Phím tắt", + "noHotkeysFound": "Không Tìm Thấy Phím Tắt", + "clearSearch": "Làm Sạch Thanh Tìm Kiếm", + "hotkeys": "Phím Tắt" + }, + "modelManager": { + "modelConverted": "Model Đã Được Chuyển Đổi", + "model": "Model", + "convertingModelBegin": "Đang chuyển đổi Model. Chờ chút.", + "hfForbidden": "Bạn không có quyền truy cập vào model HF này", + "convertToDiffusersHelpText3": "Checkpoint của bạn trên ổ đĩa SẼ bị xoá nên nó nằm trong thư mục gốc của InvokeAI. Nếu nó ở vị trí tuỳ chỉnh thì SẼ KHÔNG bị xoá.", + "modelDeleted": "Model Đã Được Xoá", + "alpha": "Alpha", + "convertToDiffusersHelpText5": "Hãy chắc chắn bạn có đủ chỗ trống trong ổ đĩa. Model thường ngốn khoảng 2-7GB.", + "convertToDiffusersHelpText6": "Bạc có chắc muốn chuyển đổi model này?", + "installAll": "Tải Xuống Toàn Bộ", + "advanced": "Nâng Cao", + "convertToDiffusers": "Đổi Sang Diffusers", + "convertToDiffusersHelpText1": "Model này sẽ được đổi sang định dạng 🧨 Diffusers.", + "modelSettings": "Thiết Lập Model", + "metadata": "Metadata", + "noDefaultSettings": "Không có thiết lập cấu hình mặc định cho model này. Hãy vào Trình Quản Lý Model để thêm thiết lập mặc định.", + "restoreDefaultSettings": "Nhấp vào để xem thiết lập mặc định của model.", + "defaultSettingsOutOfSync": "Một vài thiết lập không khớp với mặc định của model:", + "usingDefaultSettings": "Dùng thiết lập mặc định của model", + "deleteMsg1": "Bạn có chắc muốn xoá model này khỏi InvokeAI?", + "modelManager": "Quản Lý Model", + "name": "Tên", + "noModelSelected": "Không Có Model Được Chọn", + "installQueue": "Danh Sách Tải Xuống", + "modelDeleteFailed": "Xoá model thất bại", + "inplaceInstallDesc": "Tải xuống model mà không sao chép toàn bộ tài nguyên. Khi sử dụng model, nó được sẽ tải từ vị trí được đặt. Nếu bị tắt, toàn bộ tài nguyên của model sẽ được sao chép vào thư mục quản lý model của Invoke trong quá trình tải xuống.", + "modelType": "Loại Model", + "install": "Tải Xuống", + "active": "khởi động", + "addModel": "Thêm Model", + "addModels": "Thêm Model", + "allModels": "Tất Cả Model", + "clipEmbed": "CLIP Embed", + "defaultSettings": "Thiết Lập Mặc Định", + "convertToDiffusersHelpText2": "Quá trình này sẽ thay thế đầu vào của Trình Quản Lý Model bằng phiên bản Diffusers của model đó.", + "defaultSettingsSaved": "Đã Lưu Thiết Lập Mặc Định", + "description": "Dòng Mô Tả", + "imageEncoderModelId": "ID Model Image Encoder", + "hfForbiddenErrorMessage": "Chúng tôi gợi ý vào các repository. Chủ sở hữu có thể yêu cầu chấp nhận điều khoản để tải xuống.", + "hfTokenSaved": "Đã Lưu HF Token", + "learnMoreAboutSupportedModels": "Tìm hiểu thêm về những model được hỗ trợ", + "availableModels": "Model Có Sẵn", + "load": "Tải", + "cancel": "Huỷ", + "huggingFace": "HuggingFace (HF)", + "huggingFacePlaceholder": "chủ-sỡ-hữu/tên-model", + "includesNModels": "Thêm vào {{n}} model và dependency của nó.", + "localOnly": "chỉ ở trên máy chủ", + "manual": "Thủ Công", + "convertToDiffusersHelpText4": "Đây là quá trình diễn ra chỉ một lần. Nó có thể tốn tầm 30-60 giây tuỳ theo thông số kỹ thuật của máy tính.", + "edit": "Sửa", + "huggingFaceRepoID": "ID HuggingFace Repository", + "huggingFaceHelper": "Nếu nhiều model được tìm thấy trong repository này, bạn sẽ được nhắc để chọn một trong số chúng để tải.", + "modelImageDeleted": "Model Ảnh Đã Xoá", + "delete": "Xoá", + "deleteConfig": "Xoá Cấu Hình", + "modelUpdateFailed": "Cập Nhật Model Thất Bại", + "deleteMsg2": "Model trên ổ đĩa SẼ bị xoá nên nó nằm trong thư mục gốc của InvokeAI. Nếu bạn dùng ở vị trí tuỳ chỉnh thì SẼ KHÔNG bị xoá.", + "deleteModel": "Xoá Model", + "modelImageDeleteFailed": "Xoá Model Ảnh Thất Bại", + "height": "Chiều Dài", + "deleteModelImage": "Xoá Model Ảnh", + "none": "trống", + "modelImageUpdated": "Model Ảnh Đã Được Cập Nhật", + "modelImageUpdateFailed": "Cập Nhật Model Ảnh Thất Bại", + "path": "Đường Dẫn", + "noModelsInstalledDesc1": "Tải xuống model với", + "noModelsInstalled": "Chưa Tải Model", + "config": "Cấu Hình", + "convert": "Chuyển Đổi", + "baseModel": "Model Cơ Sở", + "hfTokenLabel": "HuggingFace Token (Bắt buộc cho một vài model)", + "hfTokenHelperText": "HF Token là cần thiết để sử dụng một số model. Nhấp vào đây để tạo hoặc lấy token của bạn.", + "hfTokenInvalid": "HF Token Không Hợp Lệ Hoặc Bị Thiếu", + "hfTokenInvalidErrorMessage": "HuggingFace token không hợp lệ hoặc bị thiếu.", + "hfTokenRequired": "Bạn đang tải xuống model yêu cầu HuggingFace Token hợp lệ.", + "hfTokenInvalidErrorMessage2": "Cập nhật vào ", + "hfTokenUnableToVerify": "Không Thể Xác Minh HF Token", + "hfTokenUnableToVerifyErrorMessage": "Không thể xác minh HuggingFace token. Khả năng cao lỗi mạng. Vui lòng thử lại sau.", + "inplaceInstall": "Tải Xuống Tại Chỗ", + "installRepo": "Tải Xuống Kho Lưu Trữ (Repository)", + "loraModels": "LoRA", + "main": "Chính", + "modelConversionFailed": "Chuyển Đổi Model Thất Bại", + "modelName": "Tên Model", + "modelUpdated": "Model Đã Được Cập Nhật", + "noMatchingModels": "Không Có Model Phù Hợp", + "predictionType": "Loại Prediction", + "repoVariant": "Phiên Bản Repository", + "simpleModelPlaceholder": "Url hoặc đường đẫn đến tệp hoặc thư mục chứa diffusers trong máy chủ", + "selectModel": "Chọn Model", + "spandrelImageToImage": "Hình Ảnh Sang Hình Ảnh (Spandrel)", + "starterBundles": "Gói Khởi Đầu", + "vae": "VAE", + "urlOrLocalPath": "URL / Đường Dẫn", + "triggerPhrases": "Từ Ngữ Kích Hoạt", + "variant": "Biến Thể", + "urlOrLocalPathHelper": "Url cần chỉ vào một tệp duy nhất. Còn đường dẫn trên máy chủ có thể chỉ vào một tệp hoặc một thư mục cho chỉ một model diffusers.", + "prune": "Cắt Bớt", + "uploadImage": "Tải Lên Hình Ảnh", + "syncModels": "Liên Kết Model", + "pruneTooltip": "Cắt bớt những thành phần đã hoàn tất trong hàng", + "scanPlaceholder": "Dường đẫn đến thư mục trong máy chủ", + "pathToConfig": "Đường Dẫn Đến Tệp Cấu Hình", + "search": "Tìm Kiếm", + "selected": "Đã Chọn", + "settings": "Cài Đặt", + "source": "Nguồn", + "starterBundleHelpText": "Tải toàn bộ những model cần thiết để bắt đầu với một model cơ sở, bao gồm model chính, controlnet, IP adapter, v.v... Chọn nguyên một bộ sẽ bỏ qua những model khác bạn đã tải.", + "starterModels": "Model Khởi Đầu", + "typePhraseHere": "Thêm từ ngữ ở đây", + "upcastAttention": "Upcast Attention", + "vaePrecision": "Độ Chuẩn VAE", + "installingBundle": "Đang Tải Nguyên Bộ", + "installingModel": "Đang Tải Model", + "installingXModels_other": "Đang tải {{count}} model", + "skippingXDuplicates_other": ", bỏ qua {{count}} thành phần bị lặp lại", + "repo_id": "ID Repository", + "scanFolder": "Quét Thư Mục", + "scanFolderHelper": "Thư mục sẽ được quét để tìm model. Có thể sẽ mất nhiều thời gian với những thư mục lớn.", + "scanResults": "Kết Quả", + "t5Encoder": "T5 Encoder", + "mainModelTriggerPhrases": "Từ Ngữ Kích Hoạt Cho Model Chính", + "textualInversions": "Bộ Đảo Ngược Văn Bản", + "loraTriggerPhrases": "Từ Ngữ Kích Hoạt Cho LoRA", + "width": "Chiều Rộng", + "clipLEmbed": "CLIP-L Embed", + "clipGEmbed": "CLIP-G Embed", + "controlLora": "LoRA Điều Khiển Được", + "urlUnauthorizedErrorMessage2": "Tìm hiểu thêm.", + "urlForbidden": "Bạn không có quyền truy cập vào model này", + "urlForbiddenErrorMessage": "Bạn có thể cần yêu cầu quyền truy cập từ trang web đang cung cấp model.", + "urlUnauthorizedErrorMessage": "Bạn có thể cần thiếp lập một token API để dùng được model này.", + "fluxRedux": "FLUX Redux", + "sigLip": "SigLIP", + "llavaOnevision": "LLaVA OneVision", + "fileSize": "Kích Thước Tệp", + "modelPickerFallbackNoModelsInstalled2": "Nhấp vào Trình Quản Lý Model để tải.", + "modelPickerFallbackNoModelsInstalled": "Không Có Sẵn Model.", + "manageModels": "Quản Lý Model", + "hfTokenReset": "Làm Mới HF Token", + "relatedModels": "Model Liên Quan", + "installedModelsCount": "Đã tải {{installed}} trên {{total}} model.", + "allNModelsInstalled": "Đã tải tất cả {{count}} model", + "nToInstall": "Còn {{count}} để tải", + "nAlreadyInstalled": "Có {{count}} đã tải", + "bundleAlreadyInstalled": "Gói đã được cài sẵn", + "bundleAlreadyInstalledDesc": "Tất cả model trong gói {{bundleName}} đã được cài sẵn.", + "launchpadTab": "Launchpad", + "launchpad": { + "welcome": "Chào mừng đến Trình Quản Lý Model", + "description": "Invoke yêu cầu tải model nhằm tối ưu hoá các tính năng trên nền tảng. Chọn tải các phương án thủ công hoặc khám phá các model khởi đầu thích hợp.", + "manualInstall": "Tải Thủ Công", + "urlDescription": "Tải model bằng URL hoặc đường dẫn trên máy. Phù hợp để cụ thể model muốn thêm vào.", + "huggingFaceDescription": "Duyệt và cài đặt model từ các repository trên HuggingFace.", + "scanFolderDescription": "Quét một thư mục trên máy để tự động tra và tải model.", + "recommendedModels": "Model Khuyến Nghị", + "exploreStarter": "Hoặc duyệt tất cả model khởi đầu có sẵn", + "bundleDescription": "Các gói đều bao gồm những model cần thiết cho từng nhánh model và những model cơ sở đã chọn lọc để bắt đầu.", + "sdxl": "SDXL", + "quickStart": "Gói Khởi Đầu Nhanh", + "browseAll": "Hoặc duyệt tất cả model có sẵn:", + "stableDiffusion15": "Stable Diffusion 1.5", + "fluxDev": "FLUX.1 dev" + }, + "installBundle": "Tải Xuống Gói", + "installBundleMsg1": "Bạn có chắc chắn muốn tải xuống gói {{bundleName}}?", + "installBundleMsg2": "Gói này sẽ tải xuống {{count}} model sau đây:", + "filterModels": "Lọc Model", + "ipAdapters": "IP Adapters", + "showOnlyRelatedModels": "Liên Quan", + "starterModelsInModelManager": "Model Khởi Đầu có thể tìm thấy ở Trình Quản Lý Model" + }, + "metadata": { + "guidance": "Hướng Dẫn", + "noRecallParameters": "Không tìm thấy tham số", + "imageDetails": "Chi Tiết Ảnh", + "createdBy": "Được Tạo Bởi", + "canvasV2Metadata": "Layer Canvas", + "parameterSet": "Dữ liệu tham số {{parameter}}", + "positivePrompt": "Lệnh Tích Cực", + "seed": "Hạt Giống", + "negativePrompt": "Lệnh Tiêu Cực", + "noImageDetails": "Không tìm thấy chi tiết ảnh", + "strength": "Mức độ mạnh từ ảnh sang ảnh", + "Threshold": "Ngưỡng Nhiễu", + "width": "Chiều Rộng", + "steps": "Số Bước", + "vae": "VAE", + "workflow": "Workflow", + "seamlessXAxis": "Trục X Liền Mạch", + "seamlessYAxis": "Trục Y Liền Mạch", + "cfgScale": "Thang CFG", + "allPrompts": "Tất Cả Lệnh", + "generationMode": "Chế Độ Tạo Sinh", + "height": "Chiều Dài", + "metadata": "Metadata", + "model": "Model", + "cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)", + "recallParameters": "Gợi Nhớ Tham Số", + "scheduler": "Scheduler", + "noMetaData": "Không tìm thấy metadata", + "imageDimensions": "Kích Thước Ảnh", + "clipSkip": "$t(parameters.clipSkip)", + "parsingFailed": "Lỗi Cú Pháp", + "recallParameter": "Gợi Nhớ {{label}}" + }, + "accordions": { + "generation": { + "title": "Generation (Tạo Sinh)" + }, + "image": { + "title": "Hình Ảnh" + }, + "advanced": { + "title": "Nâng Cao", + "options": "Lựa Chọn $t(accordions.advanced.title)" + }, + "compositing": { + "coherenceTab": "Coherence Pass (Liên Kết)", + "title": "Kết Hợp", + "infillTab": "Infill (Lấp Đầy)" + }, + "control": { + "title": "Điều Khiển" + } + }, + "invocationCache": { + "disableSucceeded": "Bộ Nhớ Đệm Đã Tắt", + "disableFailed": "Có Vấn Đề Khi Tắt Bộ Nhớ Đệm", + "hits": "Số Lần Trúng", + "maxCacheSize": "Tối Đa", + "cacheSize": "Tổng Cache", + "enableFailed": "Có Vấn Đề Khi Bật Bộ Nhớ Đệm", + "disable": "Tắt", + "invocationCache": "Bộ Nhớ Đệm", + "clearSucceeded": "Bộ Nhớ Đệm Đã Được Dọn", + "enableSucceeded": "Bộ Nhớ Đệm Đã Bật", + "useCache": "Dùng Bộ Nhớ Đệm", + "enable": "Bật", + "misses": "Số Lần Trật", + "clear": "Dọn Dẹp", + "clearFailed": "Có Vấn Đề Khi Dọn Dẹp Bộ Nhớ Đệm" + }, + "hrf": { + "metadata": { + "enabled": "Đã Bật Sửa Độ Phân Giải Cao", + "strength": "Mức Độ Mạnh Của Sửa Độ Phân Giải Cao", + "method": "Cách Thức Sửa Độ Phân Giải Cao" + }, + "hrf": "Sửa Độ Phân Giải Cao", + "enableHrf": "Bật Chế Độ Chỉnh Sửa Phân Giải Cao", + "upscaleMethod": "Phương Thức Upscale" + }, + "nodes": { + "validateConnectionsHelp": "Ngăn chặn những kết nối không hợp lý được tạo ra, và đồ thị không hợp lệ bị kích hoạt", + "nodeOpacity": "Độ Mờ Đục Của Node", + "nodeVersion": "Phiên Bản Của Node", + "clearWorkflowDesc": "Dọn workflow này và bắt đầu cái mới?", + "enum": "Dữ Liệu Cố Định", + "newWorkflow": "Workflow Mới", + "integer": "Số Nguyên", + "workflowHelpText": "Cần hỗ trợ? Xem hướng dẫn ở Làm Quen Với Workflow.", + "scheduler": "Scheduler", + "snapToGridHelp": "Gắn các node vào lưới khi di chuyển", + "showMinimapnodes": "HIển Thị Bản Đồ Thu Nhỏ", + "newWorkflowDesc2": "Workflow hiện tại của bạn vẫn chưa lưu các thay đổi.", + "unableToValidateWorkflow": "Không Thể Xác Thực Workflow", + "inputFieldTypeParseError": "Không thể phân tích loại dữ liệu đầu vào của {{node}}.{{field}} ({{message}})", + "boolean": "Đúng/Sai", + "missingInvocationTemplate": "Thiếu mẫu trình bày kích hoạt", + "nodeOutputs": "Đầu Ra Của Node", + "unableToUpdateNodes_other": "Không thể cập nhật {{count}} node", + "notesDescription": "Thêm ghi chú vào workflow", + "noConnectionInProgress": "Không có kết nối nào đang diễn ra", + "float": "Số Thực", + "missingNode": "Thiếu node kích hoạt", + "currentImage": "Hình Ảnh Hiện Tại", + "unknownErrorValidatingWorkflow": "Lỗi không rõ khi xác thực workflow", + "workflowSettings": "Cài Đặt Biên Tập Workflow", + "workflowVersion": "Phiên Bản", + "unableToGetWorkflowVersion": "Không thể tìm phiên bản của lược đồ workflow", + "collection": "Đa tài nguyên", + "cannotMixAndMatchCollectionItemTypes": "Không thể trộn và kết nối với loại đa tài nguyên", + "colorCodeEdges": "Mã Màu Kết Nối", + "ipAdapter": "IP Adapter", + "cannotDuplicateConnection": "Không thể tạo hai kết nối trùng lặp", + "workflowValidation": "Lỗi Xác Thực Workflow", + "sourceNodeFieldDoesNotExist": "Kết nối không phù hợp: nguồn/đầu ra của vùng {{node}}.{{field}} không tồn tại", + "targetNodeFieldDoesNotExist": "Kết nối không phù hợp: đích đến/đầu vào của vùng {{node}}.{{field}} không tồn tại", + "missingTemplate": "Node không hợp lệ: node {{node}} thuộc loại {{type}} bị thiếu mẫu trình bày (chưa tải?)", + "unsupportedMismatchedUnion": "Dạng số lượng dữ liệu không khớp với {{firstType}} và {{secondType}}", + "betaDesc": "Trình kích hoạt này vẫn trong giai đoạn beta. Cho đến khi ổn định, nó có thể phá hỏng thay đổi trong khi cập nhật ứng dụng. Chúng tôi dự định hỗ trợ trình kích hoạt này về lâu dài.", + "cannotConnectInputToInput": "Không thế kết nối đầu vào với đầu vào", + "showEdgeLabelsHelp": "Hiển thị tên trên kết nối, chỉ ra những node được kết nối", + "unsupportedArrayItemType": "loại mảng không được hỗ trợ: \"{{type}}\"", + "boardAccessError": "Không thể tìm thấy bảng {{board_id}}, chuyển về mặc định", + "collectionOrScalarFieldType": "{{name}} (Đơn/Đa)", + "edge": "Kết Nối", + "graph": "Đồ Thị", + "workflowAuthor": "Tác Giả", + "showEdgeLabels": "Hiển Thị Tên Kết Nối", + "unknownField": "Vùng Dữ Liệu Không Rõ", + "executionStateCompleted": "Đã Hoàn Tất", + "loadingNodes": "Đang Tải Node...", + "singleFieldType": "{{name}} (Đơn)", + "clearWorkflowDesc2": "Workflow hiện tại của bạn vẫn chưa lưu các thay đổi.", + "clearWorkflow": "Dọn Dẹp Workflow", + "unableToParseFieldType": "không thể phân tích vùng dữ liệu", + "allNodesUpdated": "Cập Nhật Tất Cả Node", + "noGraph": "Không Có Đồ Thị", + "collectionFieldType": "{{name}} (Đa)", + "noOutputRecorded": "Chưa có đầu ra được ghi nhận", + "noNodeSelected": "Không có node được chọn", + "snapToGrid": "Gắn Vào Lưới", + "unknownFieldType": "Loại $t(nodes.unknownField): {{type}}", + "zoomOutNodes": "Phóng Nhỏ", + "deletedInvalidEdge": "Xoá kết nối không hợp lệ {{source}} -> {{target}}", + "unableToExtractSchemaNameFromRef": "không thể trích xuất tên lược đồ từ tham chiếu", + "nodePack": "Gói node", + "workflowDescription": "Mô Tả Ngắn", + "prototypeDesc": "Trình kích hoạt này chỉ mới là bản mẫu. Nó có thể phá hỏng thay đổi trong khi cập nhật ứng dụng và có thể bị xoá bất cứ lúc nào.", + "updateNode": "Cập Nhật Node", + "noWorkflow": "Không Có Workflow", + "loadWorkflow": "Tải Workflow", + "nodeSearch": "Tìm node", + "unableToExtractEnumOptions": "không thể trích xuất lựa chọn trong dữ liệu cố định", + "node": "Node", + "nodeTemplate": "Mẫu Trình Bày Của Node", + "nodeType": "Loại Node", + "notes": "Ghi Chú", + "updateApp": "Cập Nhật Ứng Dụng", + "updateAllNodes": "Cập Nhật Các Node", + "zoomInNodes": "Phóng To", + "imageAccessError": "Không thể tìm thấy ảnh {{image_name}}, chuyển về mặc định", + "unknownNode": "Node Không Rõ", + "unknownNodeType": "Loại Node Không Rõ", + "cannotConnectOutputToOutput": "Không thế kết nối đầu ra với đầu ra", + "cannotConnectToSelf": "Không thể kết nối với chính nó", + "workflow": "Workflow", + "addNodeToolTip": "Thêm Node (Shift+A, Space)", + "animatedEdges": "Hoạt Hoạ Các Kết Nối", + "animatedEdgesHelp": "Hoạt hoạ kết nối được chọn và các kết nối liên kết với node được chọn", + "colorCodeEdgesHelp": "Mã màu kết nối dựa theo vùng kết nối của nó", + "currentImageDescription": "Hiển thị hình ảnh hiện tại trong Trình Biên Tập Node", + "missingFieldTemplate": "Thiếu vùng mẫu trình bày", + "downloadWorkflow": "Tải Xuống Workflow Dưới Dạng JSON", + "executionStateError": "Lỗi", + "fieldTypesMustMatch": "Loại của vùng cần giống nhau", + "fitViewportNodes": "Chế Độ Xem Vừa Khớp", + "fullyContainNodes": "Bao Phủ Node Hoàn Toàn Để Chọn", + "fullyContainNodesHelp": "Node phải được phủ kín hoàn toàn trong hộp lựa chọn để được lựa chọn", + "hideMinimapnodes": "Ẩn Bản Đồ Thu Nhỏ", + "inputMayOnlyHaveOneConnection": "Đầu vào chỉ có thể có một kết nối", + "noWorkflows": "Không Có Workflow", + "noMatchingWorkflows": "Không Có Workflow Phù Hợp", + "sourceNodeDoesNotExist": "Kết nối không phù hợp: nguồn/đầu ra của node {{node}} không tồn tại", + "targetNodeDoesNotExist": "Kết nối không phù hợp: đích đến/đầu vào của node {{node}} không tồn tại", + "noFieldsViewMode": "Workflow này chưa có vùng được chọn để hiển thị. Xem workflow đầy đủ để tuỳ chỉnh dữ liệu.", + "problemSettingTitle": "Có Vấn Đề Khi Thiết Lập Tiêu Đề", + "resetToDefaultValue": "Đặt lại giá trị mặc định", + "reloadNodeTemplates": "Tải Lại Mẫu Trình Bày Node", + "viewMode": "Dùng Chế Độ Xem Tuyến Tính", + "newWorkflowDesc": "Tạo workflow mới?", + "string": "Chuỗi Ký Tự", + "version": "Phiên Bản", + "workflowContact": "Thông Tin Liên Lạc", + "workflowName": "Tên", + "saveToGallery": "Lưu Vào Thư Viện Ảnh", + "connectionWouldCreateCycle": "Kết nối này sẽ tạo ra vòng lặp", + "addNode": "Thêm Node", + "unsupportedAnyOfLength": "quá nhiều dữ liệu hợp nhất: {{count}}", + "validateConnections": "Xác Thực Kết Nối Và Đồ Thị", + "workflowNotes": "Ghi Chú", + "workflowTags": "Nhãn", + "editMode": "Chỉnh sửa trong Trình Biên Tập Workflow", + "edit": "Chỉnh Sửa", + "executionStateInProgress": "Đang Xử Lý", + "outputFieldTypeParseError": "Không thể phân tích loại dữ liệu đầu ra của {{node}}.{{field}} ({{message}})", + "modelAccessError": "Không thể tìm thấy model {{key}}, chuyển về mặc định", + "internalDesc": "Trình kích hoạt này được dùng bên trong bởi Invoke. Nó có thể phá hỏng thay đổi trong khi cập nhật ứng dụng và có thể bị xoá bất cứ lúc nào.", + "specialDesc": "Trình kích hoạt này có một số xử lý đặc biệt trong ứng dụng. Ví dụ, Node Hàng Loạt được dùng để xếp vào nhiều đồ thị từ một workflow.", + "addItem": "Thêm Mục", + "linearDistribution": "Phân Bố Tuyến Tính", + "uniformRandomDistribution": "Phân Bố Ngẫu Nhiên Đồng Nhất", + "parseString": "Phân Tích Chuỗi", + "noBatchGroup": "không có nhóm", + "generatorNoValues": "trống", + "splitOn": "Tách Ở", + "arithmeticSequence": "Cấp Số Cộng", + "generatorNRandomValues_other": "{{count}} giá trị ngẫu nhiên", + "generatorLoadFromFile": "Tải Từ Tệp", + "dynamicPromptsRandom": "Dynamic Prompts (Ngẫu Nhiên)", + "dynamicPromptsCombinatorial": "Dynamic Prompts (Tổ Hợp)", + "missingSourceOrTargetNode": "Thiếu nguồn hoặc node mục tiêu", + "missingSourceOrTargetHandle": "Thiếu nguồn hoặc mục tiêu xử lý", + "deletedMissingNodeFieldFormElement": "Xóa vùng nhập bị thiếu: vùng {{fieldName}} của node {{nodeId}}", + "description": "Mô Tả", + "loadWorkflowDesc": "Tải workflow?", + "loadWorkflowDesc2": "Workflow hiện tại của bạn có những điều chỉnh chưa được lưu.", + "nodeName": "Tên Node", + "unableToUpdateNode": "Cập nhật node thất bại: node {{node}} thuộc dạng {{type}} (có thể cần xóa và tạo lại)", + "downloadWorkflowError": "Lỗi tải xuống workflow", + "generatorImagesFromBoard": "Ảnh Từ Bảng", + "generatorImagesCategory": "Phân Loại", + "generatorImages_other": "{{count}} ảnh", + "unknownField_withName": "Vùng Dữ Liệu Không Rõ \"{{name}}\"", + "unexpectedField_withName": "Sai Vùng Dữ Liệu \"{{name}}\"", + "unknownFieldEditWorkflowToFix_withName": "Workflow chứa vùng dữ liệu không rõ \"{{name}}\".\nHãy biên tập workflow để sửa lỗi.", + "missingField_withName": "Thiếu Vùng Dữ Liệu \"{{name}}\"", + "layout": { + "autoLayout": "Bố Cục Tự Động", + "layeringStrategy": "Chiến Lược Phân Layer", + "networkSimplex": "Network Simplex", + "longestPath": "Đường Đi Dài Nhất", + "nodeSpacing": "Khoảng Cách Node", + "layerSpacing": "Khoảng Cách Layer", + "layoutDirection": "Hướng Bố Cục", + "layoutDirectionRight": "Phải", + "layoutDirectionDown": "Xuống", + "alignment": "Căn Chỉnh Node", + "alignmentUL": "Trên Cùng Bên Trái", + "alignmentDL": "Dưới Cùng Bên Trái", + "alignmentUR": "Trên Cùng Bên Phải", + "alignmentDR": "Dưới Cùng Bên Phải" + }, + "generatorLoading": "đang tải", + "addLinearView": "Thêm Vào Chế Độ Xem Tuyến Tính (Linear View)", + "hideLegendNodes": "Ẩn Vùng Nhập", + "mismatchedVersion": "Node không hợp lệ: node {{node}} thuộc loại {{type}} có phiên bản không khớp (thử cập nhật?)", + "noFieldsLinearview": "Không có vùng được thêm vào Chế Độ Xem Tuyến Tính", + "removeLinearView": "Xoá Khỏi Chế Độ Xem Tuyến Tính", + "reorderLinearView": "Sắp Xếp Lại Chế Độ Xem Tuyến Tính", + "showLegendNodes": "Hiển Thị Vùng Nhập", + "unableToLoadWorkflow": "Không Thể Tải Workflow", + "unknownTemplate": "Mẫu Trình Bày Không Rõ", + "unknownInput": "Đầu Vào Không Rõ: {{name}}", + "loadingTemplates": "Đang Tải {{name}}", + "versionUnknown": " Phiên Bản Không Rõ", + "generateValues": "Giá Trị Tạo Sinh", + "floatRangeGenerator": "Phạm Vị Tạo Sinh Số Thực", + "integerRangeGenerator": "Phạm Vị Tạo Sinh Số Nguyên" + }, + "popovers": { + "paramCFGRescaleMultiplier": { + "heading": "Hệ Số Nhân Thang CFG", + "paragraphs": [ + "Hệ số nhân điều chỉnh để hướng dẫn cho CFG, dùng cho model được huấn luyện bằng zero-terminal SNR (ztsnr).", + "Giá trị khuyến cáo là 0.7 cho những model này." + ] + }, + "refinerScheduler": { + "heading": "Scheduler", + "paragraphs": [ + "Scheduler được dùng khi tinh chế các phần nhỏ của quá trình tạo sinh.", + "Giống với scheduler để tạo sinh." + ] + }, + "paramCFGScale": { + "heading": "Thang CFG", + "paragraphs": [ + "Điều khiển mức độ lệnh tác động lên quá trình tạo sinh.", + "Giá trị của Thang CFG quá cao có thể tạo độ bão hoà quá mức và khiến ảnh tạo sinh bị méo mó. " + ] + }, + "paramScheduler": { + "heading": "Scheduler", + "paragraphs": [ + "Scheduler được dùng trong quá trình tạo sinh.", + "Mỗi scheduler định nghĩa cách thêm độ nhiễu vào hình ảnh hoặc cách cập nhật mẫu dữ liệu dự vào đầu ra của model." + ] + }, + "compositingCoherencePass": { + "heading": "Coherence Pass (Liên Kết)", + "paragraphs": [ + "Bước thứ hai trong quá trình khử nhiễu để hợp nhất với ảnh inpaint/outpaint." + ] + }, + "refinerNegativeAestheticScore": { + "heading": "Điểm Khác Tiêu Chuẩn", + "paragraphs": [ + "Trọng lượng để tạo sinh ảnh giống với ảnh có điểm tiêu chuẩn thấp, dựa vào dữ liệu huấn luyện." + ] + }, + "refinerCfgScale": { + "paragraphs": [ + "Điều khiển mức độ lệnh tác động lên quá trình tạo sinh.", + "Giống với thang CFG để tạo sinh." + ], + "heading": "Thang CFG" + }, + "refinerSteps": { + "heading": "Số Bước", + "paragraphs": [ + "Số bước diễn ra trong khi tinh chế các phần nhỏ của quá trình tạo sinh.", + "Giống với số bước để tạo sinh." + ] + }, + "paramSteps": { + "heading": "Số Bước", + "paragraphs": [ + "Số bước dùng để biểu diễn trong mỗi lần tạo sinh.", + "Số bước càng cao thường sẽ tạo ra ảnh tốt hơn nhưng ngốn nhiều thời gian hơn." + ] + }, + "paramWidth": { + "heading": "Rộng", + "paragraphs": [ + "Chiều rộng của ảnh tạo sinh. Phải là bội số của 8." + ] + }, + "inpainting": { + "heading": "Chế Độ Inpaint", + "paragraphs": [ + "Điều khiển vị trí cần sửa đổi, được chỉ dẫn theo Sức Mạnh Khử Nhiễu." + ] + }, + "rasterLayer": { + "paragraphs": [ + "Dữ liệu dựa vào pixel trên ảnh, dùng cho để tạo sinh ảnh." + ], + "heading": "Layer Raster" + }, + "creativity": { + "heading": "Độ Sáng Tạo", + "paragraphs": [ + "Độ sáng tạo điều khiển mức độ tự do được trao cho model khi thêm chi tiết. Độ sáng tạo thấp cho ra ảnh gần giống với ảnh ban đầu, trong khi độ sáng tạo cao cho phép nhiều thay đổi hơn. Khi dùng lệnh, độ phân giải cao tăng ảnh hưởng của lệnh lên đầu ra." + ] + }, + "refinerPositiveAestheticScore": { + "paragraphs": [ + "Trọng lượng để tạo sinh ảnh giống với ảnh có điểm tiêu chuẩn cao, dựa vào dữ liệu huấn luyện." + ], + "heading": "Điểm Giống Tiêu Chuẩn" + }, + "paramVAEPrecision": { + "paragraphs": [ + "Độ chính xác dùng trong khi mã hoá và giải mã VAE.", + "Chính xác một nửa/Fp16 sẽ hiệu quả hơn, đổi lại cho những thay đổi nhỏ với ảnh." + ], + "heading": "Độ Chuẩn VAE" + }, + "fluxDevLicense": { + "heading": "Giấy Phép Phi Thương Mại", + "paragraphs": [ + "Model FLUX.1 [dev] được cấp phép dưới giấy phép phi thương mại FLUX [dev]. Để dùng loại model này cho lý do thương mại trong Invoke, vào trang web chúng tôi để tìm hiểu thêm." + ] + }, + "scaleBeforeProcessing": { + "heading": "Chia Tỉ Lệ Trước Khi Xử Lý", + "paragraphs": [ + "\"Tự động\" chỉnh tỉ lệ cho vùng được chọn thành kích thước phù hợp nhất cho model trước khi tạo sinh.", + "\"Thủ công\" cho phép bạn chọn chiều rộng và chiều dài cho vùng được chọn sẽ được chia tỉ lệ trước khi tạo sinh." + ] + }, + "paramHeight": { + "paragraphs": [ + "Chiều dài của ảnh tạo sinh. Phải là bội số của 8." + ], + "heading": "Dài" + }, + "paramRatio": { + "paragraphs": [ + "Tỉ lệ khung hình của kích thước của ảnh được tạo ra.", + "Kích thước ảnh (theo số lượng pixel) tương đương với 512x512 được khuyến nghị cho model SD1.5 và kích thước tương đương với 1024x1024 được khuyến nghị cho model SDXL." + ], + "heading": "Tỉ Lệ" + }, + "seamlessTilingYAxis": { + "paragraphs": [ + "Lát khối liền mạch bức ảnh theo trục dọc." + ], + "heading": "Lát Khối Liền Mạch Trục Y" + }, + "controlNetControlMode": { + "paragraphs": [ + "Đưa thêm trọng lượng vào lệnh hoặc ControlNet." + ], + "heading": "Chế Độ Điều Khiển" + }, + "compositingMaskAdjustments": { + "paragraphs": [ + "Điều chỉnh cái lớp bao phủ." + ], + "heading": "Điều Chỉnh Lớp Phủ" + }, + "regionalGuidance": { + "paragraphs": [ + "Vẽ để chỉ dẫn nơi các yếu tố từ lệnh cần xuất hiện." + ], + "heading": "Chỉ Dẫn Khu Vực" + }, + "controlNetWeight": { + "paragraphs": [ + "Điều chỉnh mức độ layer ảnh hưởng đến quá trình xử lý tạo sinh.", + "• Trọng Lượng Lớn Hơn (.75-2): Gây ra ảnh hưởng lớn hơn lên kết quả cuối cùng.", + "• Trọng Lượng Nhỏ Hơn (0-.75): Gây ra ảnh hưởng nhỏ hơn lên kết quả cuối cùng." + ], + "heading": "Trọng Lượng" + }, + "regionalReferenceImage": { + "heading": "Ảnh Mẫu Khu Vực", + "paragraphs": [ + "Vẽ để áp dụng ảnh tham khảo vào nơi cụ thể." + ] + }, + "paramHrf": { + "paragraphs": [ + "Tạo ra ảnh chất lượng cao với độ phân giải lớn hơn giá trị tối ưu cho model. Thường được dùng để tránh trùng lập trong ảnh tạo sinh." + ], + "heading": "Cho Phép Sửa Độ Phân Giải Cao" + }, + "patchmatchDownScaleSize": { + "heading": "Downscale", + "paragraphs": [ + "Downscale xảy ra bao nhiêu lần trước khi bắt đầu infill.", + "Downscale nhiều sẽ cải thiện hiệu suất nhưng giảm chất lượng." + ] + }, + "compositingCoherenceMinDenoise": { + "paragraphs": [ + "Độ khử nhiễu nhỏ nhất cho chế độ liên kết", + "Sức mạnh khử nhiễu nhỏ nhất cho vùng liên kết khi inpaint/outpaint" + ], + "heading": "Min Khử Nhiễu" + }, + "compositingCoherenceEdgeSize": { + "paragraphs": [ + "Kích cỡ cạnh dùng cho coherence pass." + ], + "heading": "Kích Cỡ Cạnh" + }, + "compositingMaskBlur": { + "heading": "Độ Mờ Vùng", + "paragraphs": [ + "Độ mờ của phần được phủ." + ] + }, + "ipAdapterMethod": { + "paragraphs": [ + "Phương thức định nghĩa cách ảnh mẫu sẽ chỉ dẫn quá trình xử lý tạo sinh." + ], + "heading": "Cách Thức" + }, + "dynamicPrompts": { + "heading": "Dynamic Prompt", + "paragraphs": [ + "Dynamic Prompt phân tích một lệnh đơn thành nhiều lệnh.", + "Cú pháp cơ bản là \"a {red|green|blue} ball\". Nó sẽ cấu thành ba lệnh: \"a red ball\", \"a green ball\" và \"a blue ball\".", + "Bạn có thể dùng cú pháp bao nhiêu lần tuỳ thích trong một lệnh đơn, nhưng hãy chắc chắn số lệnh tạo sinh không vượt mức Số lệnh Tối đa trong cài đặt." + ] + }, + "imageFit": { + "heading": "Xếp Vừa Ảnh Ban Đầu Với Kích Thước Đầu Ra", + "paragraphs": [ + "Điều chỉnh tỉ lệ ảnh ban đầu thành chiều dài và chiều rộng của ảnh đầu ra. Khuyến cáo nên bật." + ] + }, + "noiseUseCPU": { + "paragraphs": [ + "Điều chỉnh độ nhiễu được tạo ra trên CPU hay GPU.", + "Với Độ nhiễu CPU được bật, một hạt giống cụ thể sẽ tạo ra hình ảnh giống nhau trên mọi máy.", + "Không có tác động nào đến hiệu suất khi bật Độ nhiễu CPU." + ], + "heading": "Dùng Độ Nhiễu CPU" + }, + "lora": { + "heading": "LoRA", + "paragraphs": [ + "Model nhẹ dùng để kết hợp với model cơ sở." + ] + }, + "refinerModel": { + "paragraphs": [ + "Model được dùng khi tinh chế các phần nhỏ của quá trình tạo sinh.", + "Giống với model để tạo sinh." + ], + "heading": "Model Refiner" + }, + "compositingBlurMethod": { + "heading": "Phương Thức Làm Mờ", + "paragraphs": [ + "Cách làm mờ trên vùng được phủ." + ] + }, + "controlNetBeginEnd": { + "paragraphs": [ + "Cài đặt này xác định phần xử lý khử nhiễu (trong khi tạo sinh) kết hợp với chỉ dẫn từ layer này.", + "• Bước Bắt Đầu (%): Chỉ định lúc bắt đầu áp dụng chỉ dẫn từ layer này trong quá trình tạo sinh.", + "• Bước Kết Thúc (%): Chỉ định lúc dừng áp dụng chỉ dẫn của layer này và trở về chỉ dẫn chung từ model và các thiết lập khác." + ], + "heading": "Phần Trăm Số Bước Khi Bắt Đầu/Kết Thúc" + }, + "scale": { + "heading": "Tỉ Lệ", + "paragraphs": [ + "Tỉ lệ điều khiển kích thước ảnh đầu ra, và dựa vào bội số độ phân giải ảnh đầu vào. Ví dụ upscale 2x lần lên ảnh 1024x1024 sẽ cho ra ảnh đầu ra 2048x2048." + ] + }, + "upscaleModel": { + "paragraphs": [ + "Model upscale đặt tỉ lệ hình ảnh vào kích thước đầu ra trước khi thêm vào các chi tiết. Bất kỳ model upscale được hỗ trợ đều có thể sử dụng, nhưng một số sẽ chuyên về một lĩnh vực, như là ảnh chụp hay ảnh vẽ phát thảo nét." + ], + "heading": "Model Upscale" + }, + "globalReferenceImage": { + "heading": "Ảnh Mẫu Toàn Vùng", + "paragraphs": [ + "Áp dụng ảnh tham khảo để ảnh hưởng lên toàn bộ quá trình tạo sinh." + ] + }, + "dynamicPromptsSeedBehaviour": { + "paragraphs": [ + "Điều khiển cách hạt giống được dùng khi tạo sinh từ lệnh.", + "Cứ mỗi lần lặp, một hạt giống mới sẽ được dùng. Dùng nó để khám phá những biến thể từ lệnh trên mỗi hạt giống.", + "Ví dụ, nếu bạn có 5 lệnh, mỗi ảnh sẽ dùng cùng hạt giống.", + "Một hạt giống mới sẽ được dùng cho từng ảnh. Nó tạo ra nhiều biến thể." + ], + "heading": "Hành Vi Của Hạt Giống" + }, + "paramGuidance": { + "heading": "Hướng Dẫn", + "paragraphs": [ + "Điều khiển mức độ lệnh tác động lên quá trình tạo sinh.", + "Giá trị hướng dẫn cao có thể gây bão hoà quá mức, giá trị hướng dẫn quá cao hoặc quá thấp còn có nguy cơ khiến ảnh tạo sinh bị méo mó. Hướng dẫn chỉ áp dụng cho model FLUX DEV." + ] + }, + "paramVAE": { + "paragraphs": [ + "Model được dùng để dịch đầu ra của AI thành ảnh cuối cùng." + ], + "heading": "VAE" + }, + "controlNet": { + "paragraphs": [ + "ControlNet cung cấp hướng dẫn cho quá trình tạo sinh, giúp tạo ảnh với thành phần, cấu trúc hoặc phong cách được kiểm soát, tuỳ vào model được chọn." + ], + "heading": "ControlNet" + }, + "controlNetProcessor": { + "heading": "Bộ Xử Lý", + "paragraphs": [ + "Cách thức xử lý ảnh đầu vào để hướng dẫn xử lý quá trình tạo sinh. Bộ xử lý khác như sẽ cung cấp hiệu ứng hoặc phong cách khác nhau cho ảnh được tạo sinh." + ] + }, + "paramAspect": { + "paragraphs": [ + "Tỉ lệ khung hành của ảnh tạo sinh. Điều chỉnh tỉ lệ sẽ cập nhật chiều rộng và chiều dài tương ứng.", + "\"Tối ưu hoá\" sẽ đặt chiều rộng và chiều dài vào kích thước tối ưu cho model được chọn." + ], + "heading": "Tỉ Lệ" + }, + "paramNegativeConditioning": { + "heading": "Lệnh Tiêu Cực", + "paragraphs": [ + "Quá trình tạo sinh sẽ tránh những nội dung trong lệnh tiêu cực. Dùng nó để loại bỏ nội dung khỏi đầu ra.", + "Hỗ trợ Compel Syntax và Embedding." + ] + }, + "optimizedDenoising": { + "paragraphs": [ + "Bật \"Tối Ưu Hoá Hình Ảnh Sang Hình Ảnh\" cho một thang đo Sức Mạnh Khử Nhiễu tiến dần dành cho các dạng biến đổi ảnh sang ảnh và inpaint với model Flux. Cài đặt này cải thiện khả năng điều khiển số lượng biến đổi được áp dụng lên hình ảnh, nhưng có thể được tắt nếu bạn muốn thang đo Sức Mạnh Khử Nhiễu tiêu chuẩn. Cài đặt này vẫn còn được chỉnh sửa và trong quá trình beta." + ], + "heading": "Tối Ưu Hoá Hình Ảnh Sang Hình Ảnh" + }, + "refinerStart": { + "paragraphs": [ + "Nơi trong quá trình xử lý tạo sinh mà refiner bắt đầu được dùng.", + "0 nghĩa là bộ refiner sẽ được dùng trong toàn bộ quá trình tạo sinh , 0.8 nghĩa là refiner sẽ được dùng trong 20% cuối cùng quá trình tạo sinh." + ], + "heading": "Bắt Đầu Refiner" + }, + "paramUpscaleMethod": { + "paragraphs": [ + "Cách thức dùng để upscale để Sửa Độ Phân Giải Cao." + ], + "heading": "Phương Thức Upscale" + }, + "dynamicPromptsMaxPrompts": { + "paragraphs": [ + "Giới hạn số lệnh được tạo sinh bởi Dynamic Prompt." + ], + "heading": "Số Lệnh Tối Đa" + }, + "structure": { + "paragraphs": [ + "Độ cấu trúc điều khiển mức độ của ảnh đầu ra sẽ giữ nguyên các trình bày của bản gốc. Độ cấu trúc thấp cho phép các thay đổi đáng kể, trong khi độ cấu trúc cao nghiêm khắc hơn về cách trình bày và thành phần của bản gốc." + ], + "heading": "Độ Cấu Trúc" + }, + "infillMethod": { + "heading": "Cách Infill", + "paragraphs": [ + "Cách thức infill trong quá trình inpaint/outpaint." + ] + }, + "paramDenoisingStrength": { + "paragraphs": [ + "Kiểm soát độ khác nhau giữa các ảnh được tạo sinh và layer dạng raster.", + "Sức mạnh thấp cho ảnh giống với sự kết hợp của các layer dạng raster đang hiển thị. Sức mạnh cao lại cho ảnh phụ thuộc nhiều vào lệnh.", + "Khi không có gì được hiển thị bởi các layer dạng raster, điều chỉnh này sẽ được bỏ qua." + ], + "heading": "Sức Mạnh Khử Nhiễu" + }, + "paramPositiveConditioning": { + "paragraphs": [ + "Hướng dẫn cách máy tạo sinh xử lý. Bạn nên dùng từ hoặc cụm từ.", + "Hỗ trợ cú Compel Syntax, Dynamic Prompt và Embedding." + ], + "heading": "Lệnh Tích Cực" + }, + "controlNetResizeMode": { + "heading": "Chế Độ Điều Chỉnh Kích Thước", + "paragraphs": [ + "Phương thức để đặt kích thước ảnh đầu vào của Control Adapter lên kích thước đầu ra." + ] + }, + "paramSeed": { + "paragraphs": [ + "Điều khiển độ nhiễu ban đầu được dùng để tạo sinh.", + "Tắt lựa chọn \"Ngẫu Nhiên\" để tạo ra kết quá y hệt nhau với cùng một thiết lập tạo sinh." + ], + "heading": "Hạt Giống" + }, + "clipSkip": { + "heading": "CLIP Skip", + "paragraphs": [ + "Bao nhiêu lớp model CLIP được bỏ qua.", + "Một số model nhất định sẽ phù hợp hơn khi đi cùng CLIP Skip." + ] + }, + "loraWeight": { + "heading": "Trọng Lượng", + "paragraphs": [ + "Trọng lượng của LoRA. Trọng lượng càng cao sẽ dẫn đến tác động càng lớn lên ảnh cuối cùng." + ] + }, + "paramIterations": { + "heading": "Vòng Lặp", + "paragraphs": [ + "Số ảnh được tạo ra.", + "Nếu Dynamic Prompt được bật, một lệnh sẽ tạo sinh ảnh bấy nhiêu lần." + ] + }, + "compositingCoherenceMode": { + "heading": "Chế Độ", + "paragraphs": [ + "Cách thức được dùng để liên kết ảnh với vùng bao phủ vừa được tạo sinh." + ] + }, + "paramModel": { + "paragraphs": [ + "Model dùng để tạo sinh. Model khác nhau được huấn luyện để chuyên vào một kết quả và nội dung tiêu chuẩn." + ], + "heading": "Model" + }, + "regionalGuidanceAndReferenceImage": { + "heading": "Chỉ Dẫn Khu Vực Và Ảnh Mẫu Khu Vực", + "paragraphs": [ + "Dành cho Chỉ Dẫn Khu Vực, vẽ để chỉ dẫn nơi các yếu tố từ lệnh cần xuất hiện.", + "Dành cho Ảnh Mẫu Khu Vực, vẽ để áp dụng ảnh tham khảo vào nơi cụ thể." + ] + }, + "seamlessTilingXAxis": { + "paragraphs": [ + "Lát khối liền mạch bức ảnh theo trục ngang." + ], + "heading": "Lát Khối Liền Mạch Trục X" + }, + "tileSize": { + "heading": "Kích Thước Khối", + "paragraphs": [ + "Điều chỉnh kích thước của khối trong quá trình upscale. Khối càng lớn, bộ nhớ được sử dụng càng nhiều, nhưng có thể tạo sinh ảnh tốt hơn.", + "Model SD1.5 mặt định là 768, trong khi SDXL mặc định là 1024. Giảm kích thước khối nếu các gặp vấn đề bộ nhớ." + ] + }, + "tileOverlap": { + "heading": "Chồng Chéo Khối", + "paragraphs": [ + "Điều chỉnh sự chồng chéo giữa các khối liền kề trong quá trình upscale. Giá trị chồng chép lớn giúp giảm sự rõ nét của các chỗ nối nhau, nhưng ngốn nhiều bộ nhớ hơn.", + "Giá trị mặc định (128) hoạt động tốt với đa số trường hợp, nhưng bạn có thể điều chỉnh cho phù hợp với nhu cầu cụ thể và hạn chế về bộ nhớ." + ] + } + }, + "models": { + "addLora": "Thêm LoRA", + "concepts": "LoRA", + "loading": "đang tải", + "lora": "LoRA", + "noRefinerModelsInstalled": "Chưa có model SDXL Refiner được tải xuống", + "defaultVAE": "VAE Mặc Định", + "noMatchingModels": "Không có Model phù hợp", + "noModelsAvailable": "Không có model", + "selectModel": "Chọn Model", + "noCompatibleLoRAs": "Không Có LoRAs Tương Thích", + "noMatchingLoRAs": "Không có LoRA phù hợp", + "noLoRAsInstalled": "Chưa có LoRA được tải xuống" + }, + "parameters": { + "postProcessing": "Xử Lý Hậu Kỳ (Shift + U)", + "symmetry": "Tính Đối Xứng", + "type": "Loại", + "seed": "Hạt Giống", + "processImage": "Xử Lý Hình Ảnh", + "useSize": "Dùng Kích Thước", + "invoke": { + "noModelSelected": "Không có model được lựa chọn", + "canvasIsFiltering": "Canvas đang bận (đang lọc)", + "canvasIsRasterizing": "Canvas đang bận (đang raster hoá)", + "canvasIsTransforming": "Canvas đang bận (đang biến đổi)", + "canvasIsCompositing": "Canvas đang bận (đang kết hợp)", + "noPrompts": "Không có lệnh được tạo", + "noNodesInGraph": "Không có node trong đồ thị", + "addingImagesTo": "Thêm ảnh vào", + "noT5EncoderModelSelected": "Không có model T5 Encoder được lựa chọn cho máy tạo sinh FLUX", + "noFLUXVAEModelSelected": "Không có model VAE được lựa chọn cho máy tạo sinh FLUX", + "noCLIPEmbedModelSelected": "Không có model CLIP Embed được lựa chọn cho máy tạo sinh FLUX", + "systemDisconnected": "Hệ thống mất kết nối", + "invoke": "Kích Hoạt", + "missingNodeTemplate": "Thiếu mẫu trình bày node", + "missingInputForField": "thiếu đầu vào", + "missingFieldTemplate": "Thiếu vùng mẫu trình bày", + "collectionTooFewItems": "quá ít mục, tối thiểu là {{minItems}}", + "collectionTooManyItems": "quá nhiều mục, tối đa là {{maxItems}}", + "canvasIsSelectingObject": "Canvas đang bận (đang chọn đồ vật)", + "fluxModelMultipleControlLoRAs": "Chỉ có thể dùng 1 LoRA Điều Khiển Được", + "collectionStringTooLong": "quá dài, tối đa là {{maxLength}}", + "collectionStringTooShort": "quá ngắn, tối thiểu là {{minLength}}", + "collectionNumberGTMax": "{{value}} > {{maximum}} (giá trị tối đa)", + "collectionNumberLTMin": "{{value}} < {{minimum}} (giá trị tối thiểu)", + "collectionNumberNotMultipleOf": "{{value}} không phải bội của {{multipleOf}}", + "collectionNumberLTExclusiveMin": "{{value}} <= {{exclusiveMinimum}} (giá trị chọn lọc tối thiểu)", + "collectionNumberGTExclusiveMax": "{{value}} >= {{exclusiveMaximum}} (giá trị chọn lọc tối đa)", + "batchNodeCollectionSizeMismatch": "Kích cỡ tài nguyên không phù hợp với Lô {{batchGroupId}}", + "batchNodeNotConnected": "Node Hàng Loạt chưa được kết nối: {{label}}", + "batchNodeEmptyCollection": "Một vài node hàng loạt có tài nguyên rỗng", + "collectionEmpty": "tài nguyên trống", + "batchNodeCollectionSizeMismatchNoGroupId": "tài nguyên theo nhóm có kích thước sai lệch", + "modelIncompatibleBboxWidth": "Chiều rộng hộp giới hạn là {{width}} nhưng {{model}} yêu cầu bội số của {{multiple}}", + "modelIncompatibleBboxHeight": "Chiều dài hộp giới hạn là {{height}} nhưng {{model}} yêu cầu bội số của {{multiple}}", + "modelIncompatibleScaledBboxHeight": "Chiều dài hộp giới hạn theo tỉ lệ là {{height}} nhưng {{model}} yêu cầu bội số của {{multiple}}", + "modelIncompatibleScaledBboxWidth": "Chiều rộng hộp giới hạn theo tỉ lệ là {{width}} nhưng {{model}} yêu cầu bội số của {{multiple}}", + "modelDisabledForTrial": "Tạo sinh với {{modelName}} là không thể với tài khoản trial. Vào phần thiết lập tài khoản để nâng cấp.", + "promptExpansionPending": "Trong quá trình mở rộng lệnh", + "promptExpansionResultPending": "Hãy chấp thuận hoặc huỷ bỏ kết quả mở rộng lệnh của bạn", + "emptyBatches": "lô trống", + "noStartingFrameImage": "Chưa có khung hình ảnh đầu", + "fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), chiều rộng hộp giới hạn là {{width}}", + "fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), chiều cao hộp giới hạn là {{height}}", + "fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), tỉ lệ chiều rộng hộp giới hạn là {{width}}", + "fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), tỉ lệ chiều cao hộp giới hạn là {{height}}", + "incompatibleLoRAs": "LoRA không tương thích bị thêm vào" + }, + "cfgScale": "Thang CFG", + "useSeed": "Dùng Hạt Giống", + "imageActions": "Hành Động Với Hình Ảnh", + "steps": "Số Bước", + "aspect": "Tỉ Lệ", + "coherenceMode": "Chế Độ", + "coherenceEdgeSize": "Kích Cỡ Cạnh", + "coherenceMinDenoise": "Min Khử Nhiễu", + "denoisingStrength": "Sức Mạnh Khử Nhiễu", + "infillMethod": "Cách Infill", + "setToOptimalSize": "Tối ưu hoá kích cỡ cho model", + "maskBlur": "Độ Mờ Vùng", + "width": "Rộng", + "scale": "Tỉ Lệ", + "recallMetadata": "Gợi Lại Metadata", + "clipSkip": "CLIP Skip", + "general": "Cài Đặt Chung", + "boxBlur": "Làm Mờ Dạng Box", + "gaussianBlur": "Làm Mờ Dạng Gaussian", + "staged": "Staged (Tăng khử nhiễu có hệ thống)", + "scaledHeight": "Tỉ Lệ Dài", + "cancel": { + "cancel": "Huỷ" + }, + "infillColorValue": "Màu Lấp Đầy", + "optimizedImageToImage": "Tối Ưu Hoá Hình Ảnh Sang Hình Ảnh", + "sendToCanvas": "Gửi Vào Canvas", + "sendToUpscale": "Gửi Vào Upscale", + "scaledWidth": "Tỉ Lệ Rộng", + "scheduler": "Scheduler", + "seamlessXAxis": "Trục X Liền Mạch", + "seamlessYAxis": "Trục Y Liền Mạch", + "guidance": "Hướng Dẫn", + "height": "Dài", + "noiseThreshold": "Ngưỡng Nhiễu", + "negativePromptPlaceholder": "Lệnh Tiêu Cực", + "iterations": "Lặp Lại", + "strength": "Sức Mạnh", + "perlinNoise": "Nhiễu Loại Perlin", + "positivePromptPlaceholder": "Lệnh Tích Cực", + "scaleBeforeProcessing": "Tỉ Lệ Trước Khi Xử Lý", + "patchmatchDownScaleSize": "Downscale", + "useAll": "Dùng Tất Cả", + "useCpuNoise": "Dùng Độ Nhiễu CPU", + "remixImage": "Phối Lại Hình Ảnh", + "shuffle": "Xáo Trộn", + "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (lớn quá)", + "cfgRescaleMultiplier": "Hệ Số Nhân Thang CFG", + "setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (nhỏ quá)", + "images": "Ảnh Ban Đầu", + "controlNetControlMode": "Chế Độ Điều Khiển", + "lockAspectRatio": "Khoá Tỉ Lệ", + "swapDimensions": "Hoán Đổi Kích Thước", + "copyImage": "Sao Chép Hình Ảnh", + "imageFit": "Căn Chỉnh Ảnh Ban Đầu Thành Kích Thước Đầu Ra", + "info": "Thông Tin", + "usePrompt": "Dùng Lệnh", + "upscaling": "Upscale", + "tileSize": "Kích Thước Khối", + "disabledNoRasterContent": "Đã Tắt (Không Có Nội Dung Dạng Raster)", + "modelDisabledForTrial": "Tạo sinh với {{modelName}} là không thể với tài khoản trial. Vào phần thiết lập tài khoản để nâng cấp.", + "useClipSkip": "Dùng CLIP Skip", + "duration": "Thời Lượng", + "downloadImage": "Tải Xuống Hình Ảnh", + "images_withCount_other": "Hình Ảnh", + "showOptionsPanel": "Hiển Thị Bảng Bên Cạnh (O hoặc T)", + "resolution": "Độ Phân Giải" + }, + "dynamicPrompts": { + "seedBehaviour": { + "perIterationDesc": "Sử dụng hạt giống khác nhau cho mỗi lần lặp lại", + "perPromptDesc": "Sử dụng hạt giống khác nhau cho mỗi hình ảnh", + "label": "Hành Động Cho Hạt Giống", + "perPromptLabel": "Một Hạt Giống Mỗi Ảnh", + "perIterationLabel": "Hạt Giống Mỗi Lần Lặp Lại" + }, + "loading": "Tạo Sinh Bằng Dynamic Prompt...", + "showDynamicPrompts": "HIện Dynamic Prompt", + "maxPrompts": "Số Lệnh Tối Đa", + "promptsPreview": "Xem Trước Lệnh", + "dynamicPrompts": "Dynamic Prompt", + "promptsToGenerate": "Lệnh Để Tạo Sinh" + }, + "settings": { + "beta": "Beta", + "general": "Cài Đặt Chung", + "confirmOnDelete": "Xác Nhận Khi Xoá", + "developer": "Nhà Phát Triển", + "confirmOnNewSession": "Xác Nhận Khi Mở Phiên Mới", + "antialiasProgressImages": "Xử Lý Khử Răng Cưa Hình Ảnh", + "models": "Models", + "informationalPopoversDisabledDesc": "Hộp thoại hỗ trợ thông tin đã tắt. Bật lại trong Cài đặt.", + "enableModelDescriptions": "Bật Trình Mô Tả Model Bằng Hộp Thả", + "enableNSFWChecker": "Bật Trình Kiểm Tra NSFW", + "clearIntermediatesWithCount_other": "Dọn sạch {{count}} sản phẩm trung gian", + "reloadingIn": "Tải lại trong", + "resetWebUIDesc1": "Khởi động lại giao diện web chỉ làm mới bộ nhớ đệm của trình duyệt về ảnh và các thiết lập. Nó không hề xoá bất kỳ ảnh nào trong ổ đĩa.", + "intermediatesCleared_other": "Đã dọn {{count}} sản phẩm trung gian", + "generation": "Máy Tạo Sinh", + "enableInformationalPopovers": "Bật Hộp Thoại Hỗ Trợ Thông Tin", + "clearIntermediates": "Dọn Sạch Sản Phẩm Trung Gian", + "clearIntermediatesDisabled": "Hàng đợi phải trống để dọn dẹp các sản phẩm trung gian", + "clearIntermediatesDesc1": "Dọn dẹp các sản phẩm trung gian sẽ làm mới trạng thái của Canvas và ControlNet.", + "clearIntermediatesDesc2": "Các sản phẩm ảnh trung gian là sản phẩm phụ trong quá trình tạo sinh, khác với ảnh trong thư viện ảnh. Xoá sản phẩm trung gian sẽ giúp làm trống ổ đĩa.", + "resetWebUI": "Khởi Động Lại Giao Diện Web", + "showProgressInViewer": "Hiển Thị Hình Ảnh Đang Xử Lý Trong Trình Xem", + "ui": "Giao Diện Người Dùng", + "clearIntermediatesDesc3": "Ảnh trong thư viện ảnh sẽ không bị xoá.", + "informationalPopoversDisabled": "Hộp Thoại Hỗ Trợ Thông Tin Đã Tắt", + "resetComplete": "Giao diện web đã được khởi động lại.", + "resetWebUIDesc2": "Nếu ảnh không được xuất hiện trong thư viện ảnh hoặc điều gì đó không ổn đang diễn ra, hãy thử khởi động lại trước khi báo lỗi trên Github.", + "displayInProgress": "Hiển Thị Hình Ảnh Đang Xử Lý", + "intermediatesClearedFailed": "Có Vấn Đề Khi Dọn Sạch Sản Phẩm Trung Gian", + "enableInvisibleWatermark": "Bật Chế Độ Ẩn Watermark", + "showDetailedInvocationProgress": "Hiện Dữ Liệu Xử Lý", + "enableHighlightFocusedRegions": "Nhấn Mạnh Khu Vực Chỉ Định", + "modelDescriptionsDisabled": "Trình Mô Tả Model Bằng Hộp Thả Đã Tắt", + "modelDescriptionsDisabledDesc": "Trình mô tả model bằng hộp thả đã tắt. Bật lại trong Cài đặt." + }, + "sdxl": { + "loading": "Đang Tải...", + "posAestheticScore": "Điểm Giống Tiêu Chuẩn", + "steps": "Số Bước", + "refinerSteps": "Số Bước Refiner", + "refinermodel": "Model Refiner", + "refinerStart": "Bắt Đầu Refiner", + "denoisingStrength": "Sức Mạnh Khử Nhiễu", + "scheduler": "Scheduler", + "refiner": "Refiner", + "cfgScale": "Thang CFG", + "negAestheticScore": "Điểm Khác Tiêu Chuẩn", + "noModelsAvailable": "Không có sẵn model", + "concatPromptStyle": "Liên Kết Lệnh & Phong Cách", + "freePromptStyle": "Viết Thủ Công Lệnh Phong Cách", + "negStylePrompt": "Điểm Tiêu Cực Cho Lệnh Phong Cách", + "posStylePrompt": "Điểm Tích Cực Cho Lệnh Phong Cách" + }, + "controlLayers": { + "width": "Chiều Rộng", + "negativePrompt": "Lệnh Tiêu Cực", + "removeBookmark": "Bỏ Đánh Dấu", + "saveBboxToGallery": "Lưu Hộp Giới Hạn Vào Thư Viện Ảnh", + "global": "Toàn Vùng", + "pullBboxIntoReferenceImageError": "Có Vấn Đề Khi Chuyển Hộp Giới Hạn Thành Ảnh Mẫu", + "clearHistory": "Xoá Lịch Sử", + "recalculateRects": "Tính Toán Lại Hình Chữ Nhật", + "mergeVisibleOk": "Đã gộp layer", + "saveLayerToAssets": "Lưu Layer Vào Khu Tài Nguyên", + "canvas": "Canvas", + "savedToGalleryOk": "Đã Lưu Vào Thư Viện Ảnh", + "clipToBbox": "Chuyển Nét Thành Hộp Giới Hạn", + "moveToFront": "Chuyển Lên Trước", + "mergeVisible": "Gộp Layer Đang Hiển Thị", + "savedToGalleryError": "Lỗi khi lưu vào thư viện ảnh", + "moveToBack": "Chuyển Về Sau", + "moveBackward": "Chuyển Xuống Cuối", + "newGlobalReferenceImageError": "Có Vấn Đề Khi Tạo Ảnh Mẫu Toàn Vùng", + "newRegionalReferenceImageOk": "Đã Tạo Ảnh Mẫu Khu Vực", + "newControlLayerOk": "Đã Tạo Layer Điều Khiển Được", + "newControlLayerError": "Có Vấn Đề Khi Tạo Layer Điều Khiển Được", + "newRasterLayerOk": "Đã Tạo Layer Dạng Raster", + "pullBboxIntoLayerOk": "Chuyển Hợp Giới Hạn Thành Layer", + "newGlobalReferenceImageOk": "Đã Tạo Ảnh Mẫu Toàn Vùng", + "newRegionalReferenceImageError": "Có Vấn Đề Khi Tạo Ảnh Mẫu Khu Vực", + "newRasterLayerError": "Có Vấn Đề Khi Tạo Layer Dạng Raster", + "pullBboxIntoLayerError": "Có Vấn Đề Khi Chuyển Hộp Giới Hạn Thành Layer", + "pullBboxIntoReferenceImageOk": "Chuyển Hộp Giới Hạn Thành Ảnh Mẫu", + "clearCaches": "Xoá Bộ Nhớ Đệm", + "outputOnlyMaskedRegions": "Chỉ Xuất Đầu Ra Ở Vùng Tạo Sinh", + "addLayer": "Thêm Layer", + "regional": "Khu Vực", + "regionIsEmpty": "Vùng được chọn trống", + "bookmark": "Đánh Dấu Để Đổi Nhanh", + "saveCanvasToGallery": "Lưu Canvas Vào Thư Viện Ảnh", + "cropLayerToBbox": "Xén Layer Vào Hộp Giới Hạn", + "mergeDown": "Gộp Xuống", + "mergeVisibleError": "Lỗi khi gộp layer", + "bboxOverlay": "Hiển Thị Lớp Phủ Trên Hộp Giới Hạn", + "duplicate": "Nhân Bản", + "moveForward": "Chuyển Lên Đầu", + "fitBboxToLayers": "Xếp Vừa Hộp Giới Hạn Vào Layer", + "ipAdapterMethod": { + "full": "Phong Cách Và Thành Phần", + "style": "Phong Cách (Đơn Giản)", + "composition": "Chỉ Lấy Thành Phần", + "ipAdapterMethod": "Cách Thức", + "compositionDesc": "Áp dụng cách trình bày và bỏ qua phong cách mẫu.", + "fullDesc": "Áp dụng phong cách trực quan (màu, cấu tạo) & thành phần (cách trình bày).", + "styleDesc": "Áp dụng phong cách trực quan (màu, cấu tạo) và bỏ qua cách trình bày. Tên trước đây là Chỉ Lấy Phong Cách.", + "styleStrong": "Phong Cách (Mạnh Mẽ)", + "styleStrongDesc": "Áp dụng cách trình bày mạnh mẽ, với một chút giảm nhẹ ảnh hưởng lên thành phần.", + "stylePrecise": "Phong Cách (Chính Xác)", + "stylePreciseDesc": "Áp dụng cách trình bày chính xác, loại bỏ các chủ thể ảnh hưởng." + }, + "rasterLayer": "Layer Dạng Raster", + "disableAutoNegative": "Tắt Tự Động Đảo Chiều", + "controlLayer": "Layer Điều Khiển Được", + "enableTransparencyEffect": "Bật Hiệu Ứng Trong Suốt", + "deleteSelected": "Xoá Phần Được Chọn", + "showHUD": "Hiển Thị HUD", + "autoNegative": "Tự Động Đảo Chiều", + "replaceLayer": "Thay Đổi Layer", + "regionalGuidance": "Chỉ Dẫn Khu Vực", + "newCanvasFromImage": "Canvas Mới Từ Ảnh", + "convertRasterLayerTo": "Chuyển Đổi $t(controlLayers.rasterLayer) Thành", + "convertControlLayerTo": "Chuyển Đổi $t(controlLayers.controlLayer) Thành", + "convertInpaintMaskTo": "Chuyển Đổi $t(controlLayers.inpaintMask) Thành", + "convertRegionalGuidanceTo": "Chuyển Đổi $t(controlLayers.regionalGuidance) Thành", + "copyInpaintMaskTo": "Sao Chép $t(controlLayers.inpaintMask) Tới", + "copyRegionalGuidanceTo": "Sao Chép $t(controlLayers.regionalGuidance) Tới", + "newControlLayer": "$t(controlLayers.controlLayer) Mới", + "newRasterLayer": "$t(controlLayers.rasterLayer) Mới", + "enableAutoNegative": "Bật Tự Động Đảo Chiều", + "sendToCanvas": "Chuyển Tới Canvas", + "hidingType": "Ẩn {{type}}", + "copyToClipboard": "Sao Chép Vào Clipboard", + "logDebugInfo": "Thông Tin Log Gỡ Lỗi", + "regionalReferenceImage": "Ảnh Mẫu Khu Vực", + "newLayerFromImage": "Layer Mới Từ Ảnh", + "fill": { + "fillStyle": "Kiểu Lấp Đầy", + "fillColor": "Màu Lấp Đầy", + "grid": "Theo Lưới", + "diagonal": "Đường Chéo", + "horizontal": "Đường Ngang", + "crosshatch": "Đường Chéo Song Song (Crosshatch)", + "vertical": "Đường Dọc", + "solid": "Chắc Chắn", + "bgFillColor": "Màu Nền", + "fgFillColor": "Màu Nổi" + }, + "addControlLayer": "Thêm $t(controlLayers.controlLayer)", + "inpaintMask": "Lớp Phủ Inpaint", + "dynamicGrid": "Lưới Dynamic", + "layer_other": "Layer", + "pullBboxIntoLayer": "Chuyển Hộp Giới Hạn Vào Layer", + "addInpaintMask": "Thêm $t(controlLayers.inpaintMask)", + "addRegionalGuidance": "Thêm $t(controlLayers.regionalGuidance)", + "unlocked": "Mở Khoá", + "addReferenceImage": "Thêm $t(controlLayers.referenceImage)", + "inpaintMask_withCount_other": "Lớp Phủ Inpaint", + "regionalGuidance_withCount_other": "Chỉ Dẫn Khu Vực", + "rasterLayer_withCount_other": "Layer Dạng Raster", + "copyRasterLayerTo": "Sao Chép $t(controlLayers.rasterLayer) Tới", + "copyControlLayerTo": "Sao Chép $t(controlLayers.controlLayer) Tới", + "newRegionalGuidance": "$t(controlLayers.regionalGuidance) Mới", + "pullBboxIntoReferenceImage": "Chuyển Hộp Giới Hạn Vào Ảnh Mẫu", + "maskFill": "Lấp Đầy Lớp Phủ", + "addRasterLayer": "Thêm $t(controlLayers.rasterLayer)", + "referenceImage": "Ảnh Mẫu", + "showProgressOnCanvas": "Hiện Quá Trình Xử Lý Lên Canvas", + "prompt": "Lệnh", + "beginEndStepPercentShort": "Phần Trăm Bắt Đầu/Kết Thúc", + "weight": "Trọng Lượng", + "controlMode": { + "controlMode": "Chế Độ Điều Khiển", + "balanced": "Cân Bằng (khuyến khích)", + "prompt": "Lệnh", + "control": "Điều Khiển", + "megaControl": "Siêu Điều Khiển" + }, + "addPositivePrompt": "Thêm $t(controlLayers.prompt)", + "deleteReferenceImage": "Xoá Ảnh Mẫu", + "disableTransparencyEffect": "Tắt Hiệu Ứng Trong Suốt", + "opacity": "Độ Mờ Đục", + "rectangle": "Hình Chữ Nhật", + "addNegativePrompt": "Thêm $t(controlLayers.negativePrompt)", + "globalReferenceImage": "Ảnh Mẫu Toàn Vùng", + "controlLayer_withCount_other": "Layer Điều Khiển Được", + "newInpaintMask": "$t(controlLayers.inpaintMask) Mới", + "locked": "Khoá", + "transparency": "Độ Trong Suốt", + "showingType": "Hiển Thị {{type}}", + "selectObject": { + "invertSelection": "Đảo Ngược Phần Chọn", + "include": "Bao Gồm", + "exclude": "Loại Trừ", + "reset": "Làm Mới", + "saveAs": "Lưu Như", + "dragToMove": "Kéo kiểm để di chuyển nó", + "clickToAdd": "Nhấp chuột vào layer để thêm điểm", + "clickToRemove": "Nhấp chuột vào một điểm để xoá", + "selectObject": "Chọn Đối Tượng", + "pointType": "Loại Điểm", + "neutral": "Trung Hoà", + "apply": "Áp Dụng", + "cancel": "Huỷ Bỏ", + "process": "Xử Lý" + }, + "canvasContextMenu": { + "saveBboxToGallery": "Lưu Hộp Giới Hạn Vào Thư Viện Ảnh", + "newGlobalReferenceImage": "Ảnh Mẫu Toàn Vùng Mới", + "cropCanvasToBbox": "Xén Canvas Vào Hộp Giới Hạn", + "newRegionalGuidance": "Chỉ Dẫn Khu Vực Mới", + "saveToGalleryGroup": "Lưu Vào Thư Viện Ảnh", + "newInpaintMask": "Lớp Phủ Inpaint Mới", + "saveCanvasToGallery": "Lưu Canvas Vào Thư Viện Ảnh", + "newRegionalReferenceImage": "Ảnh Mẫu Khu Vực Mới", + "newControlLayer": "Layer Điều Khiển Được Mới", + "newRasterLayer": "Layer Dạng Raster Mới", + "bboxGroup": "Được Tạo Từ Hộp Giới Hạn", + "canvasGroup": "Canvas", + "copyCanvasToClipboard": "Sao Chép Canvas Vào Clipboard", + "copyToClipboard": "Sao Chép Vào Clipboard", + "copyBboxToClipboard": "Sao Chép Hộp Giới Hạn Vào Clipboard", + "newResizedControlLayer": "Layer Điều Khiển Được Đã Chỉnh Kích Thước Mới" + }, + "stagingArea": { + "saveToGallery": "Lưu Vào Thư Viện Ảnh", + "accept": "Chấp Nhận", + "discard": "Bỏ Đi", + "previous": "Trước", + "next": "Sau", + "showResultsOn": "Hiển Thị Kết Quả", + "discardAll": "Bỏ Đi Tất Cả", + "showResultsOff": "Ẩn Đi Kết Quả" + }, + "filter": { + "dw_openpose_detection": { + "draw_face": "Vẽ Mặt", + "description": "Phát hiện tư thế người trong layer được chọn bằng model DW Openpose.", + "draw_hands": "Vẽ Tay", + "label": "Trình Phát Hiện DW Openpose", + "draw_body": "Vẽ Cơ Thể" + }, + "hed_edge_detection": { + "label": "Trình Phát Hiện HED Edge", + "description": "Tạo ra dữ liệu cạnh từ layer được chọn bằng model phát hiện HED Edge.", + "scribble": "Vẽ Nguệch Ngoạc" + }, + "canny_edge_detection": { + "low_threshold": "Ngưỡng Thấp", + "high_threshold": "Ngưỡng Cao", + "label": "Trình Phát Hiện Cạnh Canny", + "description": "Tạo sinh một dữ liệu cạnh từ layer được chọn bằng thuật toán phát hiện cạnh Canny." + }, + "depth_anything_depth_estimation": { + "label": "Depth Anything", + "model_size_small_v2": "Small v2", + "model_size": "Kích Thước Model", + "description": "Tạo dữ liệu chiều sâu từ layer được chọn bằng model Depth Anything.", + "model_size_base": "Base", + "model_size_small": "Small", + "model_size_large": "Large" + }, + "mediapipe_face_detection": { + "min_confidence": "Độ Tư Tin Tối Thiểu", + "label": "Trình Phát Hiện Mặt MediaPipe", + "description": "Phát hiện mặt trong layer được chọn bằng model phát hiện mặt MediaPipe.", + "max_faces": "Số Lượng Mặt Tối Đa" + }, + "lineart_edge_detection": { + "description": "Tạo ra dữ liệu cạnh từ layer được chọn bằng model phát hiện cạnh Lineart.", + "coarse": "Thô", + "label": "Trình Phát Hiện Cạnh Lineart" + }, + "process": "Xử Lý", + "reset": "Làm Mới", + "cancel": "Huỷ Bỏ", + "pidi_edge_detection": { + "label": "Trình Phát Hiện Cạnh PiDiNet", + "scribble": "Vẽ Nguệch Ngoạc", + "quantize_edges": "Lượng Tử Hoá Cạnh", + "description": "Tạo ra dữ liệu cạnh từ layer được chọn bằng model phát hiện cạnh PiDiNet." + }, + "spandrel_filter": { + "model": "Model", + "scale": "Tỉ Lệ Mong Muốn", + "label": "Model Hình Ảnh Sang Hình Ảnh", + "description": "Chạy model ảnh sang ảnh trên layer được chọn.", + "autoScale": "Tự Động Chỉnh Tỉ Lệ", + "autoScaleDesc": "Model được chọn sẽ chạy cho đến khi chạm đến tỉ lệ mong muốn." + }, + "filterType": "Kiểu Lọc", + "apply": "Áp Dụng", + "mlsd_detection": { + "score_threshold": "Ngưỡng Điểm", + "distance_threshold": "Ngưỡng Xa", + "label": "Trình Phát Hiện Đoạn Thẳng", + "description": "Tạo ra dữ liệu đoạn thẳng từ layer được chọn bằng model phát hiện đoạn thẳng MLSD." + }, + "content_shuffle": { + "description": "Xáo trộn nội dung của layer được chọn, giống với hiệu ứng kéo (liquify).", + "label": "Xáo Trộn Nội Dung", + "scale_factor": "Hệ Số Tỉ Lệ" + }, + "normal_map": { + "label": "Dữ Liệu Bình Thường", + "description": "Tạo một dữ liệu bình thường từ layer được chọn." + }, + "filters": "Bộ Lọc", + "autoProcess": "Tự Động Xử Lý", + "lineart_anime_edge_detection": { + "label": "Trình Phát Hiện Cạnh Lineart Anime", + "description": "Tạo ra dữ liệu cạnh từ layer được chọn bằng model phát hiện cạnh Lineart Anime." + }, + "filter": "Bộ Lọc", + "color_map": { + "description": "Tạo một dữ liệu màu từ layer được chọn.", + "tile_size": "Kích Thước Khối", + "label": "Dữ Liệu Màu" + }, + "advanced": "Nâng Cao", + "processingLayerWith": "Đang xử lý layer với bộ lọc {{type}}.", + "forMoreControl": "Để kiểm soát tốt hơn, bấm vào mục Nâng Cao bên dưới.", + "img_blur": { + "description": "Làm mờ layer được chọn.", + "blur_type": "Dạng Làm Mờ", + "blur_radius": "Radius", + "gaussian_type": "Gaussian", + "label": "Làm Mờ Ảnh", + "box_type": "Box" + }, + "img_noise": { + "salt_and_pepper_type": "Salt and Pepper", + "noise_amount": "Lượng Nhiễu", + "label": "Độ Nhiễu Ảnh", + "description": "Tăng độ nhiễu vào layer được chọn.", + "noise_type": "Dạng Nhiễu", + "gaussian_type": "Gaussian", + "noise_color": "Màu Nhiễu", + "size": "Cỡ Nhiễu" + }, + "adjust_image": { + "channel": "Kênh Màu", + "cyan": "Lục Lam (Cmyk)", + "value_setting": "Giá Trị", + "scale_values": "Giá Trị Theo Tỉ Lệ", + "red": "Đỏ (Rgba)", + "green": "Lục (rGba)", + "blue": "Lam (rgBa)", + "alpha": "Độ Trong Suốt (rgbA)", + "luminosity": "Độ Sáng (Lab)", + "magenta": "Hồng Đỏ (cMyk)", + "yellow": "Vàng (cmYk)", + "description": "Điều chỉnh kênh màu được chọn của ảnh.", + "black": "Đen (cmyK)", + "cr": "Cr (ycC)", + "label": "Điều Chỉnh Ảnh", + "value": "Độ Sáng (hsV)", + "saturation": "Độ Bão Hoà (hSv)", + "hue": "Vùng Màu (Hsv)", + "a": "A (lAb)", + "b": "B (laB)", + "y": "Y (Ycc)", + "cb": "Cb (yCc)" + } + }, + "transform": { + "fitModeCover": "Che Phủ", + "fitModeFill": "Lấp Đầy", + "transform": "Biến Hình", + "fitToBbox": "Xếp Vừa Vào Hộp Giới Hạn", + "fitMode": "Chế Độ Xếp Vừa", + "apply": "Áp Dụng", + "cancel": "Huỷ Bỏ", + "fitModeContain": "Bao Gồm", + "reset": "Làm Mới" + }, + "HUD": { + "entityStatus": { + "isHidden": "{{title}} đang được ẩn", + "isTransforming": "{{title}} đang được biến đổi", + "isEmpty": "{{title}} đang trống", + "isLocked": "{{title}} đang bị khoá", + "isFiltering": "{{title}} đang được lọc", + "isDisabled": "{{title}} đang bị tắt" + }, + "bbox": "Hộp Giới Hạn", + "scaledBbox": "Hộp Giới Hạn Được Chia Tỉ Lệ" + }, + "settings": { + "isolatedLayerPreview": "Xem Trước Layer Bị Cô Lập", + "invertBrushSizeScrollDirection": "Cuộn Ngược Lại Cho Cỡ Cọ", + "snapToGrid": { + "on": "Bật", + "label": "Gắn Vào Lưới", + "off": "Tắt" + }, + "pressureSensitivity": "Độ Nhạy Áp Lực", + "preserveMask": { + "label": "Bảo Vệ Vùng Bao Phủ", + "alert": "Đang Bảo Vệ Vùng Bao Phủ" + }, + "isolatedLayerPreviewDesc": "Có hay không hiển thị riêng layer này khi thực hiện các thao tác như lọc hay biến đổi.", + "isolatedStagingPreview": "Xem Trước Tổng Quan Phần Cô Lập", + "isolatedPreview": "Xem Trước Phần Cô Lập", + "saveAllImagesToGallery": { + "label": "Chuyển Sản Phẩm Tạo Sinh Mới Vào Thư Viện Ảnh", + "alert": "Đang chuyển sản phẩm tạo sinh mới vào Thư Viện Ảnh, bỏ qua Canvas" + } + }, + "tool": { + "eraser": "Tẩy", + "brush": "Cọ", + "rectangle": "Hình Chữ Nhật", + "bbox": "Hộp Giới Hạn", + "move": "Di Chuyển", + "view": "Công Cụ Xem", + "colorPicker": "Chọn Màu" + }, + "mergingLayers": "Đang gộp layer", + "controlLayerEmptyState": "Tải lên ảnh, kéo thả ảnh từ thư viện ảnh vào layer này, kéo hộp giới hạn vào layer này, hoặc vẽ trên canvas để bắt đầu.", + "referenceImageEmptyState": "Tải lên hình ảnh hoặc kéo ảnh từ thư viện ảnh vào Ảnh Mẫu để bắt đầu.", + "useImage": "Dùng Hình Ảnh", + "resetCanvasLayers": "Khởi Động Lại Layer Canvas", + "asRasterLayer": "Như $t(controlLayers.rasterLayer)", + "asRasterLayerResize": "Như $t(controlLayers.rasterLayer) (Thay Đổi Kích Thước)", + "asControlLayer": "Như $t(controlLayers.controlLayer)", + "asControlLayerResize": "Như $t(controlLayers.controlLayer) (Thay Đổi Kích Thước)", + "newSession": "Phiên Làm Việc Mới", + "resetGenerationSettings": "Khởi Động Lại Cài Đặt Tạo Sinh", + "referenceImageRegional": "Ảnh Mẫu (Khu Vực)", + "warnings": { + "problemsFound": "Phát hiện vấn đề", + "unsupportedModel": "layer không được hỗ trợ cho model cơ sở này", + "controlAdapterNoModelSelected": "không có model được chọn cho Layer Chỉnh Sửa Được", + "controlAdapterNoControl": "chưa chọn/vẽ điều khiển", + "ipAdapterIncompatibleBaseModel": "model cơ sở cho Ảnh Mẫu không tương thích", + "ipAdapterNoImageSelected": "chưa chọn Ảnh Mẫu", + "controlAdapterIncompatibleBaseModel": "model cơ sở cho Layer Chỉnh Sửa Được không tương thích", + "ipAdapterNoModelSelected": "không có model được chọn cho Ảnh Mẫu", + "rgNoPromptsOrIPAdapters": "không có lệnh hoặc Ảnh Mẫu", + "rgNegativePromptNotSupported": "Lệnh Tiêu Cực không được hỗ trợ cho model cơ sở được chọn", + "rgReferenceImagesNotSupported": "Ảnh Mẫu Khu Vực không được hỗ trợ cho model cơ sở được chọn", + "rgAutoNegativeNotSupported": "Tự Động Đảo Chiều không được hỗ trợ cho model cơ sở được chọn", + "rgNoRegion": "không có khu vực được vẽ", + "fluxFillIncompatibleWithControlLoRA": "LoRA Điều Khiển Được không tương tích với FLUX Fill", + "bboxHidden": "Hộp giới hạn đang ẩn (shift+o để bật/tắt)" + }, + "pasteTo": "Dán Vào", + "pasteToAssets": "Tài Nguyên", + "pasteToAssetsDesc": "Dán Vào Tài Nguyên", + "pasteToBbox": "Hộp Giới Hạn", + "pasteToBboxDesc": "Layer Mới (Trong Hộp Giới Hạn)", + "pasteToCanvas": "Canvas", + "pasteToCanvasDesc": "Layer Mới (Trong Canvas)", + "regionCopiedToClipboard": "Sao Chép {{region}} Vào Clipboard", + "copyRegionError": "Lỗi khi sao chép {{region}}", + "errors": { + "unableToLoadImage": "Không Thể Tải Hình Ảnh", + "unableToFindImage": "Không Thể Tìm Hình Ảnh" + }, + "fluxReduxImageInfluence": { + "low": "Thấp", + "lowest": "Thấp Nhất", + "high": "Cao", + "imageInfluence": "Ảnh Chi Phối", + "medium": "Vừa", + "highest": "Cao Nhất" + }, + "addDenoiseLimit": "Thêm $t(controlLayers.denoiseLimit)", + "imageNoise": "Độ Nhiễu Hình Ảnh", + "denoiseLimit": "Giới Hạn Khử Nhiễu", + "addImageNoise": "Thêm $t(controlLayers.imageNoise)", + "referenceImageEmptyStateWithCanvasOptions": "Tải lên hình ảnh, kéo ảnh từ thư viện ảnh vào Ảnh Mẫu này, hoặc kéo hộp giới hạn vào Ảnh Mẫu này để bắt đầu.", + "exportCanvasToPSD": "Xuất Canvas Thành File PSD", + "ruleOfThirds": "Hiển Thị Quy Tắc Một Phần Ba", + "showNonRasterLayers": "Hiển Thị Layer Không Thuộc Dạng Raster (Shift + H)", + "hideNonRasterLayers": "Ẩn Layer Không Thuộc Dạng Raster (Shift + H)", + "autoSwitch": { + "off": "Tắt", + "switchOnStart": "Khi Bắt Đầu", + "switchOnFinish": "Khi Kết Thúc" + }, + "fitBboxToMasks": "Xếp Vừa Hộp Giới Hạn Vào Lớp Phủ", + "invertMask": "Đảo Ngược Lớp Phủ", + "maxRefImages": "Ảnh Mẫu Tối Đa", + "useAsReferenceImage": "Dùng Làm Ảnh Mẫu", + "deletePrompt": "Xoá Lệnh", + "addGlobalReferenceImage": "Thêm $t(controlLayers.globalReferenceImage)", + "referenceImageGlobal": "Ảnh Mẫu (Toàn Vùng)", + "sendingToCanvas": "Chuyển Ảnh Tạo Sinh Vào Canvas", + "sendingToGallery": "Chuyển Ảnh Tạo Sinh Vào Thư Viện Ảnh", + "sendToGallery": "Chuyển Tới Thư Viện Ảnh", + "sendToGalleryDesc": "Bấm 'Kích Hoạt' sẽ tiến hành tạo sinh và lưu ảnh vào thư viện ảnh.", + "newImg2ImgCanvasFromImage": "Chuyển Đổi Ảnh Sang Ảnh Mới Từ Ảnh", + "sendToCanvasDesc": "Bấm 'Kích Hoạt' sẽ hiển thị công việc đang xử lý của bạn lên canvas.", + "viewProgressInViewer": "Xem quá trình xử lý và ảnh đầu ra trong Trình Xem Ảnh.", + "viewProgressOnCanvas": "Xem quá trình xử lý và ảnh đầu ra trong Canvas.", + "globalReferenceImage_withCount_other": "$t(controlLayers.globalReferenceImage)", + "regionalGuidance_withCount_hidden": "Chỉ Dẫn Khu Vực ({{count}} đang ẩn)", + "controlLayers_withCount_hidden": "Layer Điều Khiển Được ({{count}} đang ẩn)", + "rasterLayers_withCount_hidden": "Layer Dạng Raster ({{count}} đang ẩn)", + "globalReferenceImages_withCount_hidden": "Ảnh Mẫu Toàn Vùng ({{count}} đang ẩn)", + "inpaintMasks_withCount_hidden": "Lớp Phủ Inpaint ({{count}} đang ẩn)", + "regionalGuidance_withCount_visible": "Chỉ Dẫn Khu Vực ({{count}})", + "controlLayers_withCount_visible": "Layer Điều Khiển Được ({{count}})", + "rasterLayers_withCount_visible": "Layer Dạng Raster ({{count}})", + "globalReferenceImages_withCount_visible": "Ảnh Mẫu Toàn Vùng ({{count}})", + "inpaintMasks_withCount_visible": "Lớp Phủ Inpaint ({{count}})", + "layer_withCount_other": "Layer ({{count}})", + "pastedTo": "Dán Vào {{destination}}", + "stagingOnCanvas": "Hiển thị hình ảnh lên", + "newGallerySession": "Phiên Thư Viện Ảnh Mới", + "newGallerySessionDesc": "Nó sẽ dọn sạch canvas và các thiết lập trừ model được chọn. Các ảnh được tạo sinh sẽ được chuyển đến thư viện ảnh.", + "newCanvasSession": "Phiên Canvas Mới", + "newCanvasSessionDesc": "Nó sẽ dọn sạch canvas và các thiết lập trừ model được chọn. Các ảnh được tạo sinh sẽ được chuyển đến canvas.", + "replaceCurrent": "Thay Đổi Cái Hiện Tại", + "uploadOrDragAnImage": "Kéo ảnh từ thư viện ảnh hoặc tải lên ảnh." + }, + "stylePresets": { + "negativePrompt": "Lệnh Tiêu Cực", + "viewModeTooltip": "Đây là cách lệnh của bạn sẽ trông giống khi dùng với mẫu trình bày được chọn hiện tại. Để chỉnh sửa lệnh, nhấp chuột vào bất kỳ nơi nào trên hộp văn bản.", + "flatten": "Chuyển mẫu trình bày đang chọn thành lệnh hiện tại", + "promptTemplatesDesc3": "Nếu bạn bỏ quá ký tự tạm thời, mẫu trình bày sẽ được thêm vào ở cuối lệnh.", + "positivePrompt": "Lệnh Tích Cực", + "private": "Cá Nhân", + "toggleViewMode": "Tắt Chế Độ Xem", + "acceptedColumnsKeys": "Các cột/từ khoá được chấp nhận:", + "positivePromptColumn": "'prompt' hoặc 'positive_prompt'", + "noMatchingTemplates": "Không có mẫu trình bày phù hợp", + "myTemplates": "Mẫu Trình Bày Của Tôi", + "type": "Loại", + "copyTemplate": "Sao Chép Mẫu Trình Bày", + "exportFailed": "Không thể tạo ra và tải xuống CSV", + "searchByName": "Tìm theo tên", + "sharedTemplates": "Mẫu Trình Bày Nhóm", + "shared": "Nhóm", + "uploadImage": "Tải Lên Ảnh", + "deleteTemplate": "Xoá Mẫu Trình Bày", + "editTemplate": "Chỉnh Sửa Mẫu Trình Bày", + "insertPlaceholder": "Thêm ký hiệu tạm thời", + "promptTemplatesDesc1": "Mẫu trình bày cho lệnh thêm từ ngữ cho lệnh bạn viết trong hộp lệnh.", + "preview": "Xem Trước", + "updatePromptTemplate": "Cập Nhật Mẫu Trình Bày Cho Lệnh", + "negativePromptColumn": "'negative_prompt'", + "useForTemplate": "Dùng Cho Mẫu Trình Bày Cho Lệnh", + "choosePromptTemplate": "Chọn Mẫu Trình Bày Cho Lệnh", + "defaultTemplates": "Mẫu Trình Bày Mặc Định", + "deleteTemplate2": "Bạn có chắc muốn xoá mẫu trình bày này? Không đi lại được đâu.", + "active": "Hiệu Lực", + "promptTemplatesDesc2": "Dùng ký tự tạm thời
{{placeholder}}
để cụ thể hoá nơi lệnh nên được bao gồm trong mẫu trình bày.", + "viewList": "Xem Danh Sách Mẫu Trình Bày", + "createPromptTemplate": "Tạo Mẫu Trình Bày Cho Lệnh", + "nameColumn": "'name'", + "name": "Tên", + "importTemplates": "Nhập Vào Mẫu Trình Bày Cho Lệnh (CSV/JSON)", + "clearTemplateSelection": "Dọn Sạch Mẫu Trình Bày Đã Chọn", + "exportDownloaded": "Xuất Mẫu Đã Tải Xuống", + "noTemplates": "Không có mẫu trình bày", + "promptTemplateCleared": "Mẫu Trình Bày Cho Lệnh Đã Được Dọn", + "deleteImage": "Xoá Hình Ảnh", + "exportPromptTemplates": "Xuất Mẫu Trình Bày Cho Lệnh Ra (CSV)", + "templateDeleted": "Mẫu trình bày cho lệnh đã được xoá", + "unableToDeleteTemplate": "Không thể xoá mẫu trình bày cho lệnh", + "togglePromptPreviews": "Bật/Tắt Xem Trước Lệnh" + }, + "system": { + "enableLogging": "Bật Chế Độ Ghi Log", + "logNamespaces": { + "models": "Models", + "gallery": "Thư Viện Ảnh", + "config": "Cấu Hình", + "queue": "Queue", + "workflows": "Workflow", + "events": "Sự Kiện", + "metadata": "Metadata", + "generation": "Generation", + "system": "Hệ Thống", + "canvas": "Canvas", + "logNamespaces": "Vùng Ghi Log", + "dnd": "Kéo Thả" + }, + "logLevel": { + "logLevel": "Cấp Độ Log", + "error": "Error", + "fatal": "Fatal", + "trace": "Trace", + "warn": "Warn", + "debug": "Debug", + "info": "Info" + } + }, + "toast": { + "imageUploadFailed": "Tải Lên Ảnh Thất Bại", + "layerCopiedToClipboard": "Sao Chép Layer Vào Clipboard", + "imageCopied": "Ảnh Đã Được Sao Chép", + "sentToUpscale": "Chuyển Vào Upscale", + "unableToLoadImage": "Không Thể Tải Hình Ảnh", + "unableToLoadStylePreset": "Không Thể Tải Phong Cách Được Cài Đặt Trước", + "stylePresetLoaded": "Phong Cách Được Cài Đặt Trước Đã Tải", + "unableToLoadImageMetadata": "Không Thể Tải Metadata Của Ảnh", + "workflowLoaded": "Workflow Đã Tải", + "uploadFailed": "Tải Lên Thất Bại", + "uploadFailedInvalidUploadDesc": "Phải là ảnh PNG, JPEG hoặc WEBP.", + "serverError": "Lỗi Server", + "addedToBoard": "Thêm vào tài nguyên của bảng {{name}}", + "sessionRef": "Phiên: {{sessionId}}", + "sentToCanvas": "Chuyển Vào Canvas", + "importFailed": "Nhập Vào Thất Bại", + "importSuccessful": "Nhập Vào Thành Công", + "workflowDeleted": "Workflow Đã Xoá", + "connected": "Kết Nối Đến Server", + "imageUploaded": "Ảnh Đã Được Tải Lên", + "modelImportCanceled": "Nhập Vào Model Thất Bại", + "parameters": "Tham Số", + "parameterSet": "Gợi Lại Tham Số", + "parameterSetDesc": "Gợi lại {{parameter}}", + "loadedWithWarnings": "Đã Tải Workflow Với Cảnh Báo", + "outOfMemoryErrorDesc": "Thiết lập tạo sinh hiện tại đã vượt mức cho phép của thiết bị. Hãy điều chỉnh thiết lập và thử lại.", + "problemRetrievingWorkflow": "Có Vấn Đề Khi Lấy Lại Workflow", + "somethingWentWrong": "Có Vấn Đề Phát Sinh", + "problemDeletingWorkflow": "Có Vấn Đề Khi Xoá Workflow", + "parameterNotSet": "Tham Số Không Được Gợi Lại", + "parameterNotSetDescWithMessage": "Không thể gợi lại {{parameter}}: {{message}}", + "parametersNotSet": "Tham Số Không Được Gợi Lại", + "errorCopied": "Lỗi Khi Sao Chép", + "prunedQueue": "Cắt Bớt Hàng Đợi", + "imagesWillBeAddedTo": "Ảnh đã tải lên sẽ được thêm vào tài nguyên của bảng {{boardName}}.", + "baseModelChangedCleared_other": "Cập nhật, dọn sạch hoặc tắt {{count}} model phụ không tương thích", + "canceled": "Quá Trình Xử Lý Đã Huỷ", + "baseModelChanged": "Model Cơ Sở Đã Đổi", + "addedToUncategorized": "Thêm vào tài nguyên của bảng $t(boards.uncategorized)", + "linkCopied": "Đường Liên Kết Đã Được Sao Chép", + "outOfMemoryError": "Lỗi Vượt Quá Bộ Nhớ", + "modelAddedSimple": "Đã Thêm Model Vào Hàng Đợi", + "parametersSet": "Tham Số Đã Được Gợi Lại", + "parameterNotSetDesc": "Không thể gợi lại {{parameter}}", + "problemCopyingImage": "Không Thể Sao Chép Ảnh", + "problemDownloadingImage": "Không Thể Tải Xuống Ảnh", + "problemCopyingLayer": "Không Thể Sao Chép Layer", + "problemSavingLayer": "Không Thể Lưu Layer", + "outOfMemoryErrorDescLocal": "Làm theo hướng dẫn VRAM Thấp của chúng tôi để hạn chế OOM (Tràn bộ nhớ).", + "unableToCopy": "Không Thể Sao Chép", + "unableToCopyDesc_theseSteps": "các bước sau", + "unableToCopyDesc": "Trình duyệt của bạn không hỗ trợ tính năng clipboard. Người dùng Firefox có thể khắc phục theo ", + "pasteSuccess": "Dán Vào {{destination}}", + "pasteFailed": "Dán Thất Bại", + "fluxFillIncompatibleWithT2IAndI2I": "FLUX Fill không tương tích với Từ Ngữ Sang Hình Ảnh và Hình Ảnh Sang Hình Ảnh. Dùng model FLUX khác cho các tính năng này.", + "problemUnpublishingWorkflowDescription": "Có vấn đề khi ngừng đăng tải workflow. Vui lòng thử lại sau.", + "workflowUnpublished": "Workflow Đã Được Ngừng Đăng Tải", + "problemUnpublishingWorkflow": "Có Vấn Đề Khi Ngừng Đăng Tải Workflow", + "chatGPT4oIncompatibleGenerationMode": "ChatGPT 4o chỉ hỗ trợ Từ Ngữ Sang Hình Ảnh và Hình Ảnh Sang Hình Ảnh. Hãy dùng model khác cho các tác vụ Inpaint và Outpaint.", + "imagenIncompatibleGenerationMode": "Google {{model}} chỉ hỗ trợ Từ Ngữ Sang Hình Ảnh. Dùng các model khác cho Hình Ảnh Sang Hình Ảnh, Inpaint và Outpaint.", + "fluxKontextIncompatibleGenerationMode": "FLUX Kontext không hỗ trợ tạo sinh từ hình ảnh từ canvas. Thử sử dụng Ảnh Mẫu và tắt các Layer Dạng Raster.", + "noVisibleRasterLayers": "Không Có Layer Dạng Raster Hiển Thị", + "noVisibleRasterLayersDesc": "Khởi động ít nhất một layer dạng raster để xuất file PSD", + "invalidCanvasDimensions": "Kích Thước Canvas Không Phù Hợp", + "canvasTooLarge": "Canvas Quá Lớn", + "canvasTooLargeDesc": "Kích thước canvas vượt mức tối đa cho phép để xuất file PSD. Giảm cả chiều dài và chiều rộng chủa canvas và thử lại.", + "psdExportSuccess": "Xuất File PSD Hoàn Tất", + "psdExportSuccessDesc": "Thành công xuất {{count}} layer sang file PSD", + "problemExportingPSD": "Có Vấn Đề Khi Xuất File PSD", + "canvasManagerNotAvailable": "Trình Quản Lý Canvas Không Có Sẵn", + "promptGenerationStarted": "Trình tạo sinh lệnh khởi động", + "uploadAndPromptGenerationFailed": "Thất bại khi tải lên ảnh để tạo sinh lệnh", + "promptExpansionFailed": "Có vấn đề xảy ra. Hãy thử mở rộng lệnh lại.", + "maskInverted": "Đã Đảo Ngược Lớp Phủ", + "maskInvertFailed": "Thất Bại Khi Đảo Ngược Lớp Phủ", + "noVisibleMasks": "Không Có Lớp Phủ Đang Hiển Thị", + "noVisibleMasksDesc": "Tạo hoặc bật ít nhất một lớp phủ inpaint để đảo ngược", + "imageNotLoadedDesc": "Không thể tìm thấy ảnh", + "imageSaved": "Ảnh Đã Lưu", + "imageSavingFailed": "Lưu Ảnh Thất Bại", + "invalidUpload": "Dữ Liệu Tải Lên Không Hợp Lệ", + "layerSavedToAssets": "Lưu Layer Vào Khu Tài Nguyên", + "noRasterLayers": "Không Tìm Thấy Layer Dạng Raster", + "noRasterLayersDesc": "Tạo ít nhất một layer dạng raster để xuất file PSD", + "noActiveRasterLayers": "Không Có Layer Dạng Raster Hoạt Động", + "noActiveRasterLayersDesc": "Bật ít nhất một layer dạng raster để xuất file PSD", + "failedToProcessLayers": "Thất Bại Khi Xử Lý Layer", + "noValidLayerAdapters": "Không có Layer Adaper Phù Hợp", + "setControlImage": "Đặt làm ảnh điều khiển được", + "setNodeField": "Đặt làm vùng node", + "uploadFailedInvalidUploadDesc_withCount_other": "Cần tối đa {{count}} ảnh PNG, JPEG, hoặc WEBP.", + "noInpaintMaskSelected": "Không Có Lớp Phủ Inpant Được Chọn", + "noInpaintMaskSelectedDesc": "Chọn một lớp phủ inpaint để đảo ngược", + "invalidBbox": "Hộp Giới Hạn Không Hợp Lệ", + "invalidBboxDesc": "Hợp giới hạn có kích thước không hợp lệ" + }, + "ui": { + "tabs": { + "gallery": "Thư Viện Ảnh", + "models": "Models", + "upscaling": "Upscale (Nâng Cấp Chất Lượng Hình Ảnh)", + "canvas": "Canvas (Vùng Ảnh)", + "upscalingTab": "$t(common.tab) $t(ui.tabs.upscaling)", + "modelsTab": "$t(common.tab) $t(ui.tabs.models)", + "queue": "Queue (Hàng Đợi)", + "workflows": "Workflow (Luồng Làm Việc)", + "workflowsTab": "$t(common.tab) $t(ui.tabs.workflows)", + "generate": "Tạo Sinh" + }, + "launchpad": { + "workflowsTitle": "Đi sâu hơn với Workflow.", + "upscalingTitle": "Upscale và thêm chi tiết.", + "canvasTitle": "Biên tập và làm đẹp trên Canvas.", + "generateTitle": "Tạo sinh ảnh từ lệnh chữ.", + "modelGuideText": "Muốn biết lệnh nào tốt nhất cho từng model chứ?", + "modelGuideLink": "Xem thêm Hướng Dẫn Model.", + "workflows": { + "description": "Workflow là các template tái sử dụng được sẽ tự động hoá các tác vụ tạo sinh ảnh, cho phép bạn nhanh chóng thực hiện cách thao tác phức tạp và nhận được kết quả nhất quán.", + "learnMoreLink": "Học thêm cách tạo ra workflow", + "browseTemplates": { + "title": "Duyệt Template Workflow", + "description": "Chọn từ các workflow có sẵn cho những tác vụ cơ bản" + }, + "createNew": { + "title": "Tạo workflow mới", + "description": "Tạo workflow mới từ ban đầu" + }, + "loadFromFile": { + "title": "Tải workflow từ tệp", + "description": "Tải lên workflow để bắt đầu với những thiết lập sẵn có" + } + }, + "upscaling": { + "uploadImage": { + "title": "Tải Ảnh Để Upscale", + "description": "Nhấp hoặc kéo ảnh để upscale (JPG, PNG, WebP lên đến 100MB)" + }, + "replaceImage": { + "title": "Thay Thế Ảnh Hiện Tại", + "description": "Nhấp hoặc kéo ảnh mới để thay thế cái hiện tại" + }, + "imageReady": { + "title": "Ảnh Đã Sẵn Sàng", + "description": "Bấm 'Kích Hoạt' để chuẩn bị upscale" + }, + "readyToUpscale": { + "title": "Chuẩn bị upscale!", + "description": "Điều chỉnh thiết lập bên dưới, sau đó bấm vào nút 'Khởi Động' để chuẩn bị upscale ảnh." + }, + "upscaleModel": "Model Upscale", + "model": "Model", + "helpText": { + "promptAdvice": "Khi upscale, dùng lệnh để mô tả phương thức và phong cách. Tránh mô tả các chi tiết cụ thể trong ảnh.", + "styleAdvice": "Upscale thích hợp nhất cho phong cách chung của ảnh." + }, + "scale": "Kích Thước", + "creativityAndStructure": { + "title": "Độ Sáng Tạo & Cấu Trúc Mặc Định", + "conservative": "Bảo toàn", + "balanced": "Cân bằng", + "creative": "Sáng tạo", + "artistic": "Thẩm mỹ" + } + }, + "createNewWorkflowFromScratch": "Tạo workflow mới từ đầu", + "browseAndLoadWorkflows": "Duyệt và tải workflow có sẵn", + "addStyleRef": { + "title": "Thêm Phong Cách Mẫu", + "description": "Thêm ảnh để chuyển đổi diện mạo của nó." + }, + "editImage": { + "title": "Biên Tập Ảnh", + "description": "Thêm ảnh để chỉnh sửa." + }, + "generateFromText": { + "title": "Tạo Sinh Từ Chữ", + "description": "Nhập lệnh vào và Kích Hoạt." + }, + "useALayoutImage": { + "title": "Dùng Bố Cục Ảnh", + "description": "Thêm ảnh để điều khiển bố cục." + }, + "generate": { + "canvasCalloutTitle": "Đang tìm cách để điều khiển, chỉnh sửa, và làm lại ảnh?", + "canvasCalloutLink": "Vào Canvas cho nhiều tính năng hơn." + } + }, + "panels": { + "launchpad": "Launchpad", + "workflowEditor": "Trình Biên Tập Workflow", + "imageViewer": "Trình Xem", + "canvas": "Canvas" + } + }, + "workflows": { + "delete": "Xoá", + "descending": "Giảm Dần", + "created": "Đã Tạo", + "edit": "Chỉnh Sửa", + "download": "Tải Xuống", + "copyShareLink": "Sao Chép Liên Kết Chia Sẻ", + "deleteWorkflow2": "Bạn có chắc muốn xoá workflow này không? Không có đi lại được đâu.", + "workflowSaved": "Workflow Đã Được Lưu", + "saveWorkflowAs": "Lưu Workflow Như", + "downloadWorkflow": "Lưu Vào Tệp", + "noWorkflows": "Không Có Workflow", + "savingWorkflow": "Đang Lưu Workflow...", + "ascending": "Tăng Dần", + "loading": "Đang Tải Workflow", + "chooseWorkflowFromLibrary": "Chọn Workflow Từ Thư Viện", + "workflows": "Workflow", + "copyShareLinkForWorkflow": "Sao Chép Liên Kết Chia Sẻ Cho Workflow", + "name": "Tên", + "unnamedWorkflow": "Workflow Vô Danh", + "saveWorkflow": "Lưu Workflow", + "problemSavingWorkflow": "Có Vấn Đề Khi Lưu Workflow", + "updated": "Đã Cập Nhật", + "uploadWorkflow": "Tải Từ Tệp", + "autoLayout": "Bố Trí Tự Động", + "loadWorkflow": "$t(common.load) Workflow", + "newWorkflowCreated": "Workflow Mới Được Tạo", + "workflowCleared": "Đã Dọn Dẹp Workflow", + "loadFromGraph": "Tải Workflow Từ Đồ Thị", + "convertGraph": "Chuyển Đổi Đồ Thị", + "saveWorkflowToProject": "Lưu Workflow Vào Dự Án", + "workflowName": "Tên Workflow", + "workflowLibrary": "Thư Viện Workflow", + "opened": "Đã Mở", + "deleteWorkflow": "Xoá Workflow", + "workflowEditorMenu": "Menu Biên Tập Workflow", + "builder": { + "resetAllNodeFields": "Tải Lại Các Vùng Node", + "builder": "Trình Tạo Vùng Nhập", + "layout": "Bố Cục", + "row": "Hàng", + "zoomToNode": "Phóng To Vào Node", + "addToForm": "Thêm Vào Vùng Nhập", + "label": "Nhãn Tên", + "showDescription": "Hiện Dòng Mô Tả", + "component": "Thành Phần", + "numberInput": "Nhập Số", + "singleLine": "Một Dòng", + "multiLine": "Nhiều Dòng", + "slider": "Thanh Trượt", + "both": "Cả Hai", + "emptyRootPlaceholderEditMode": "Kéo thành phần vùng nhập hoặc vùng node vào đây để bắt đầu.", + "containerPlaceholder": "Hộp Chứa Trống", + "headingPlaceholder": "Đầu Dòng Trống", + "textPlaceholder": "Văn Bản Trống", + "column": "Cột", + "deleteAllElements": "Xóa Tất Cả Thành Phần", + "nodeField": "Vùng Node", + "nodeFieldTooltip": "Để thêm vùng node, bấm vào dấu cộng nhỏ trên vùng trong Trình Biên Tập Workflow, hoặc kéo vùng theo tên của nó vào vùng nhập.", + "container": "Hộp Chứa", + "heading": "Đầu Dòng", + "text": "Văn Bản", + "divider": "Gạch Chia", + "minimum": "Tối Thiểu", + "maximum": "Tối Đa", + "containerRowLayout": "Hộp Chứa (bố cục hàng)", + "containerColumnLayout": "Hộp Chứa (bố cục cột)", + "resetOptions": "Tải Lại Lựa Chọn", + "addOption": "Thêm Lựa Chọn", + "dropdown": "Danh Sách Thả Xuống", + "publish": "Đăng Tải", + "published": "Đã Đăng", + "workflowLocked": "Workflow Bị Khóa", + "workflowLockedDuringPublishing": "Workflow bị khóa khi đang điều chỉnh để đăng tải.", + "selectOutputNode": "Chọn Node Đầu Ra", + "changeOutputNode": "Đổi Node Đầu Ra", + "publishedWorkflowOutputs": "Đầu Ra", + "unpublishableInputs": "Những đầu vào không đăng tải được sẽ bị bỏ sót", + "noPublishableInputs": "Không có đầu vào không đăng tải được", + "noOutputNodeSelected": "Không có node đầu ra được chọn", + "publishWarnings": "Cảnh Báo", + "errorWorkflowHasUnsavedChanges": "Workflow có các thay đổi chưa lưu", + "cannotPublish": "Không thể đăng workflow", + "publishedWorkflowInputs": "Đầu Vào", + "unpublish": "Chưa Đăng", + "workflowLockedPublished": "Workflow được đăng tải sẽ bị khóa không thể biên tập.\nBạn có thể ngừng đăng để chỉnh sửa, hoặc tạo một bản sao của nó.", + "errorWorkflowHasInvalidGraph": "Đồ thị workflow không hợp lệ (di chuột đến nút Khởi Động để xem chi tiết)", + "errorWorkflowHasNoOutputNode": "Không có node đầu ra được chọn", + "warningWorkflowHasUnpublishableInputFields": "Workflow có một số đầu ra không đăng được - chúng sẽ bị bỏ sót khỏi workflow", + "publishFailed": "Đăng Tải Thất Bại", + "publishFailedDesc": "Có vấn đề khi đăng tải workflow. Xin vui lòng thử lại.", + "publishSuccessDesc": "Kiểm tra Bảng Dự Án để xem tiến độ.", + "publishingValidationRun": "Kiểm Tra Tính Hợp Lệ", + "publishedWorkflowsLocked": "Workflow đã đăng sẽ bị khóa và không thể biên tập hoặc chạy nữa. Hoặc là ngừng đăng, hoặc là lưu một bản sao của chính nó để biên tập hay chạy workflow này.", + "publishInProgress": "Quá trình đăng tải đang diễn ra", + "warningWorkflowHasNoPublishableInputFields": "Không có vùng đầu vào đăng tải được được chọn - workflow sẽ chạy với các giá trị mặc định", + "publishSuccess": "Workflow của bạn đã được đăng", + "publishedWorkflowIsLocked": "Workflow đã đăng đang bị khóa", + "publishingValidationRunInProgress": "Quá trình kiểm tra tính hợp lệ đang diễn ra.", + "selectingOutputNodeDesc": "Bấm vào node để biến nó thành node đầu ra của workflow.", + "selectingOutputNode": "Chọn node đầu ra", + "errorWorkflowHasUnpublishableNodes": "Workflow có lô node, node sản sinh, hoặc node tách metadata", + "removeFromForm": "Xóa Khỏi Vùng Nhập", + "showShuffle": "Hiện Xáo Trộn", + "shuffle": "Xáo Trộn", + "emptyRootPlaceholderViewMode": "Chọn Chỉnh Sửa để bắt đầu tạo nên một vùng nhập cho workflow này.", + "workflowBuilderAlphaWarning": "Trình tạo vùng nhập đang trong giai đoạn alpha. Nó có thể xuất hiện những thay đổi đột ngột trước khi chính thức được phát hành." + }, + "yourWorkflows": "Workflow Của Bạn", + "browseWorkflows": "Khám Phá Workflow", + "workflowThumbnail": "Ảnh Minh Họa Workflow", + "saveChanges": "Lưu Thay Đổi", + "shared": "Nhóm", + "searchPlaceholder": "Tìm theo tên, mô tả, hoặc nhãn", + "recentlyOpened": "Mở Gần Đây", + "private": "Cá Nhân", + "loadMore": "Tải Thêm", + "view": "Xem", + "deselectAll": "Huỷ Chọn Tất Cả", + "recommended": "Có Thể Bạn Sẽ Cần", + "emptyStringPlaceholder": "", + "published": "Đã Đăng", + "defaultWorkflows": "Workflow Mặc Định", + "userWorkflows": "Workflow Của Người Dùng", + "projectWorkflows": "Dự Án Workflow", + "allLoaded": "Đã Tải Tất Cả Workflow", + "filterByTags": "Lọc Theo Nhãn", + "noRecentWorkflows": "Không Có Workflows Gần Đây", + "openWorkflow": "Mở Workflow", + "problemLoading": "Có Vấn Đề Khi Tải Workflow", + "noDescription": "Không có mô tả", + "searchWorkflows": "Tìm Workflow", + "clearWorkflowSearchFilter": "Xoá Workflow Khỏi Bộ Lọc Tìm Kiếm", + "openLibrary": "Mở Thư Viện" + }, + "upscaling": { + "missingUpscaleInitialImage": "Thiếu ảnh dùng để upscale", + "scale": "Tỉ Lệ", + "upscale": "Upscale (Nâng Cấp Chất Lượng Hình Ảnh)", + "upscaleModel": "Model Upscale", + "upscaleModelDesc": "Model upscale (ảnh sang ảnh)", + "missingUpscaleModel": "Thiếu model upscale", + "missingTileControlNetModel": "Không có model ControlNet Tile phù hợp đã cài đặt", + "creativity": "Độ Sáng Tạo", + "structure": "Độ Cấu Trúc", + "exceedsMaxSize": "Thiết lập upscale vượt quá giới hạn kích thước tối đa", + "tileControlNetModelDesc": "Model ControlNet Tile dành cho phiên bản model chính đã chọn", + "exceedsMaxSizeDetails": "Giới hạn upscale tối đa là {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixel. Hãy thử lại ảnh nhỏ hơn hoặc giảm thang đo upscale xuống.", + "postProcessingModel": "Model Xử Lý Hậu Kỳ", + "mainModelDesc": "Model chính (SD1.5 hoặc SDXL)", + "postProcessingMissingModelWarning": "Đến Trình Quản Lý Model để tải model xử lý hậu kỳ (ảnh sang ảnh).", + "missingModelsWarning": "Đến Trình Quản Lý Model để tải model cần thiết:", + "incompatibleBaseModel": "Phiên bản model chính không được hỗ trợ để upscale", + "incompatibleBaseModelDesc": "Upscale chỉ hỗ trợ cho model phiên bản SD1.5 và SDXL. Đổi model chính để bật lại tính năng upscale.", + "tileControl": "Điều Chỉnh Khối", + "tileSize": "Kích Thước Khối", + "tileOverlap": "Chồng Chéo Khối" + }, + "newUserExperience": { + "toGetStartedLocal": "Để bắt đầu, hãy chắc chắn đã tải xuống hoặc thêm vào model cần để chạy Invoke. Sau đó, nhập lệnh vào hộp và nhấp chuột vào Kích Hoạt để tạo ra bức ảnh đầu tiên. Chọn một mẫu trình bày cho lệnh để cải thiện kết quả. Bạn có thể chọn để lưu ảnh trực tiếp vào Thư Viện Ảnh hoặc chỉnh sửa chúng ở Canvas.", + "gettingStartedSeries": "Cần thêm hướng dẫn? Xem thử Bắt Đầu Làm Quen để biết thêm mẹo khai thác toàn bộ tiềm năng của Invoke Studio.", + "toGetStarted": "Để bắt đầu, hãy nhập lệnh vào hộp và nhấp chuột vào Kích Hoạt để tạo ra bức ảnh đầu tiên. Chọn một mẫu trình bày cho lệnh để cải thiện kết quả. Bạn có thể chọn để lưu ảnh trực tiếp vào Thư Viện Ảnh hoặc chỉnh sửa chúng ở Canvas.", + "noModelsInstalled": "Dường như bạn chưa tải model nào cả! Bạn có thể tải xuống các model khởi đầu hoặc nhập vào thêm model.", + "lowVRAMMode": "Cho hiệu suất tốt nhất, hãy làm theo hướng dẫn VRAM Thấp của chúng tôi.", + "toGetStartedWorkflow": "Để bắt đầu, hãy điền vào khu vực bên trái và bấm Kích Hoạt nhằm tạo sinh ảnh. Muốn khám phá thêm workflow? Nhấp vào icon thư mục nằm cạnh tiêu đề workflow để xem một dãy các mẫu trình bày khác." + }, + "whatsNew": { + "whatsNewInInvoke": "Có Gì Mới Ở Invoke", + "readReleaseNotes": "Đọc Ghi Chú Phát Hành", + "watchRecentReleaseVideos": "Xem Video Phát Hành Mới Nhất", + "items": [ + "Canvas: Chia tách màu nổi và màu nền - bật/tắt với 'x', khởi động lại về dạng đen trắng với 'd'", + "LoRA: Đặt khối lượng mặc định cho LoRA trong Trình Quản Lý Model" + ], + "watchUiUpdatesOverview": "Xem Tổng Quan Về Những Cập Nhật Cho Giao Diện Người Dùng" + }, + "supportVideos": { + "supportVideos": "Video Hỗ Trợ", + "gettingStarted": "Bắt Đầu Làm Quen", + "watch": "Xem", + "studioSessionsDesc": "Tham gia để xem các buổi phát trực tiếp và đặt câu hỏi. Các phiên được đăng lên trên playlist các tuần tiếp theo.", + "videos": { + "gettingStarted": { + "title": "Bắt Đầu Với Invoke", + "description": "Hoàn thành các video bao hàm mọi thứ bạn cần biết để bắt đầu với Invoke, từ tạo bức ảnh đầu tiên đến các kỹ thuật phức tạp khác." + }, + "studioSessions": { + "title": "Phiên Studio", + "description": "Đào sâu vào các phiên họp để khám phá những tính năng nâng cao của Invoke, sáng tạo workflow, và thảo luận cộng đồng." + } + } + }, + "modelCache": { + "clearSucceeded": "Cache Model Đã Được Dọn", + "clearFailed": "Có Vấn Đề Khi Dọn Cache Model", + "clear": "Dọn Cache Model" + }, + "lora": { + "weight": "Trọng Lượng" + } +} diff --git a/invokeai/frontend/web/public/locales/zh-CN.json b/invokeai/frontend/web/public/locales/zh-CN.json new file mode 100644 index 00000000000..227f90d25fa --- /dev/null +++ b/invokeai/frontend/web/public/locales/zh-CN.json @@ -0,0 +1,1753 @@ +{ + "common": { + "hotkeysLabel": "快捷键", + "languagePickerLabel": "语言", + "reportBugLabel": "反馈错误", + "settingsLabel": "设置", + "img2img": "图生图", + "nodes": "工作流", + "upload": "上传", + "load": "加载", + "statusDisconnected": "未连接", + "accept": "同意", + "cancel": "取消", + "dontAskMeAgain": "不要再次询问", + "areYouSure": "你确认吗?", + "random": "随机", + "openInNewTab": "在新的标签页打开", + "back": "返回", + "githubLabel": "GitHub", + "discordLabel": "Discord", + "txt2img": "文生图", + "postprocessing": "后期处理", + "loading": "加载中", + "linear": "线性的", + "batch": "批次管理器", + "communityLabel": "社区", + "modelManager": "模型管理器", + "learnMore": "了解更多", + "advanced": "高级", + "t2iAdapter": "T2I Adapter", + "ipAdapter": "IP Adapter", + "controlNet": "ControlNet", + "on": "开", + "auto": "自动", + "checkpoint": "Checkpoint", + "inpaint": "内补重绘", + "simple": "简单", + "template": "模板", + "outputs": "输出", + "data": "数据", + "safetensors": "Safetensors", + "outpaint": "外扩绘制", + "details": "详情", + "format": "格式", + "unknown": "未知", + "folder": "文件夹", + "error": "错误", + "installed": "已安装", + "file": "文件", + "somethingWentWrong": "出了点问题", + "copyError": "$t(gallery.copy) 错误", + "input": "输入", + "delete": "删除", + "updated": "已上传", + "save": "保存", + "created": "已创建", + "unknownError": "未知错误", + "direction": "指向", + "orderBy": "排序方式:", + "saveAs": "保存为", + "ai": "ai", + "or": "或", + "aboutDesc": "使用 Invoke 工作?来看看:", + "add": "添加", + "copy": "复制", + "aboutHeading": "掌握你的创造力", + "enabled": "已启用", + "disabled": "已禁用", + "red": "红", + "editor": "编辑器", + "positivePrompt": "正向提示词", + "negativePrompt": "反向提示词", + "selected": "选中的", + "green": "绿", + "blue": "蓝", + "dontShowMeThese": "请勿显示这些内容", + "beta": "测试版", + "toResolve": "解决", + "tab": "标签页", + "apply": "应用", + "edit": "编辑", + "off": "关", + "loadingImage": "正在加载图片", + "ok": "确定", + "placeholderSelectAModel": "选择一个模型", + "close": "关闭", + "reset": "重设", + "none": "无", + "new": "新建", + "view": "视图", + "alpha": "透明度通道", + "openInViewer": "在查看器中打开", + "clipboard": "剪贴板", + "loadingModel": "加载模型", + "generating": "生成中" + }, + "gallery": { + "galleryImageSize": "预览大小", + "gallerySettings": "预览设置", + "autoSwitchNewImages": "自动切换到新图像", + "deleteImage_other": "删除{{count}}张图片", + "deleteImagePermanent": "删除的图片无法被恢复。", + "autoAssignBoardOnClick": "点击后自动分配面板", + "featuresWillReset": "如果您删除该图像,这些功能会立即被重置。", + "loading": "加载中", + "currentlyInUse": "该图像目前在以下功能中使用:", + "copy": "复制", + "download": "下载", + "downloadSelection": "下载所选内容", + "noImageSelected": "无选中的图像", + "deleteSelection": "删除所选内容", + "image": "图像", + "drop": "弃用", + "dropOrUpload": "$t(gallery.drop) 或上传", + "dropToUpload": "$t(gallery.drop) 以上传", + "unstarImage": "取消收藏图像", + "starImage": "收藏图像", + "alwaysShowImageSizeBadge": "始终显示图像尺寸", + "selectForCompare": "选择以比较", + "slider": "滑块", + "sideBySide": "并排", + "bulkDownloadFailed": "下载失败", + "bulkDownloadRequested": "准备下载", + "bulkDownloadRequestedDesc": "您的下载请求正在准备中,这可能需要一些时间。", + "bulkDownloadRequestFailed": "下载准备过程中出现问题", + "viewerImage": "查看器图像", + "compareImage": "对比图像", + "openInViewer": "在查看器中打开", + "hover": "悬停", + "selectAllOnPage": "选择本页全部", + "swapImages": "交换图像", + "exitBoardSearch": "退出面板搜索", + "exitSearch": "退出图像搜索", + "oldestFirst": "最旧在前", + "sortDirection": "排序方向", + "showStarredImagesFirst": "优先显示收藏的图片", + "compareHelp3": "按 C 键对调正在比较的图片。", + "showArchivedBoards": "显示已归档的面板", + "newestFirst": "最新在前", + "compareHelp4": "按 ZEsc 键退出。", + "searchImages": "按元数据搜索", + "compareHelp2": "按 M 键切换不同的比较模式。", + "displayBoardSearch": "板块搜索", + "displaySearch": "图像搜索", + "stretchToFit": "拉伸以适应", + "exitCompare": "退出对比", + "compareHelp1": "在点击图库中的图片或使用箭头键切换比较图片时,请按住Alt 键。", + "go": "运行", + "boardsSettings": "画板设置", + "imagesSettings": "画廊图片设置", + "gallery": "画廊", + "move": "移动", + "imagesTab": "您在Invoke中创建和保存的图片。", + "assetsTab": "您已上传用于项目的文件。" + }, + "hotkeys": { + "searchHotkeys": "检索快捷键", + "noHotkeysFound": "未找到快捷键", + "clearSearch": "清除检索项", + "app": { + "cancelQueueItem": { + "title": "取消", + "desc": "取消当前正在处理的队列项目。" + }, + "selectQueueTab": { + "title": "选择队列标签", + "desc": "选择队列标签。" + }, + "toggleLeftPanel": { + "desc": "显示或隐藏左侧面板。", + "title": "开关左侧面板" + }, + "resetPanelLayout": { + "title": "重设面板布局", + "desc": "将左侧和右侧面板重置为默认大小和布局。" + }, + "togglePanels": { + "title": "开关面板", + "desc": "同时显示或隐藏左右两侧的面板。" + }, + "selectWorkflowsTab": { + "title": "选择工作流标签", + "desc": "选择工作流标签。" + }, + "selectModelsTab": { + "title": "选择模型标签", + "desc": "选择模型标签。" + }, + "toggleRightPanel": { + "title": "开关右侧面板", + "desc": "显示或隐藏右侧面板。" + }, + "clearQueue": { + "title": "清除队列", + "desc": "取消并清除所有队列条目。" + }, + "selectCanvasTab": { + "title": "选择画布标签", + "desc": "选择画布标签。" + }, + "invokeFront": { + "desc": "将生成请求排队,添加到队列的前面。", + "title": "调用(前台)" + }, + "selectUpscalingTab": { + "title": "选择放大选项卡", + "desc": "选择高清放大选项卡。" + }, + "focusPrompt": { + "title": "聚焦提示", + "desc": "将光标焦点移动到正向提示。" + }, + "title": "应用程序", + "invoke": { + "title": "调用", + "desc": "将生成请求排队,添加到队列的末尾。" + } + }, + "canvas": { + "selectBrushTool": { + "title": "画笔工具", + "desc": "选择画笔工具。" + }, + "selectEraserTool": { + "title": "橡皮擦工具", + "desc": "选择橡皮擦工具。" + }, + "title": "画布", + "selectColorPickerTool": { + "title": "拾色器工具", + "desc": "选择拾色器工具。" + }, + "fitBboxToCanvas": { + "title": "使边界框适应画布", + "desc": "缩放并调整视图以适应边界框。" + }, + "setZoomTo400Percent": { + "title": "缩放到400%", + "desc": "将画布的缩放设置为400%。" + }, + "setZoomTo800Percent": { + "desc": "将画布的缩放设置为800%。", + "title": "缩放到800%" + }, + "redo": { + "desc": "重做上一次画布操作。", + "title": "重做" + }, + "nextEntity": { + "title": "下一层", + "desc": "在列表中选择下一层。" + }, + "selectRectTool": { + "title": "矩形工具", + "desc": "选择矩形工具。" + }, + "selectViewTool": { + "title": "视图工具", + "desc": "选择视图工具。" + }, + "prevEntity": { + "desc": "在列表中选择上一层。", + "title": "上一层" + }, + "transformSelected": { + "desc": "变换所选图层。", + "title": "变换" + }, + "selectBboxTool": { + "title": "边界框工具", + "desc": "选择边界框工具。" + }, + "setZoomTo200Percent": { + "title": "缩放到200%", + "desc": "将画布的缩放设置为200%。" + }, + "applyFilter": { + "title": "应用过滤器", + "desc": "将待处理的过滤器应用于所选图层。" + }, + "filterSelected": { + "title": "过滤器", + "desc": "对所选图层进行过滤。仅适用于栅格层和控制层。" + }, + "cancelFilter": { + "title": "取消过滤器", + "desc": "取消待处理的过滤器。" + }, + "incrementToolWidth": { + "title": "增加工具宽度", + "desc": "增加所选的画笔或橡皮擦工具的宽度。" + }, + "decrementToolWidth": { + "desc": "减少所选的画笔或橡皮擦工具的宽度。", + "title": "减少工具宽度" + }, + "selectMoveTool": { + "title": "移动工具", + "desc": "选择移动工具。" + }, + "cancelTransform": { + "desc": "取消待处理的变换。", + "title": "取消变换" + }, + "applyTransform": { + "title": "应用变换", + "desc": "将待处理的变换应用于所选图层。" + }, + "setZoomTo100Percent": { + "title": "缩放到100%", + "desc": "将画布的缩放设置为100%。" + }, + "resetSelected": { + "title": "重置图层", + "desc": "重置选定的图层。仅适用于修复蒙版和区域指导。" + }, + "undo": { + "title": "撤消", + "desc": "撤消上一次画布操作。" + }, + "quickSwitch": { + "title": "图层快速切换", + "desc": "在最后两个选定的图层之间切换。如果某个图层被书签标记,则始终在该图层和最后一个未标记的图层之间切换。" + }, + "fitLayersToCanvas": { + "title": "使图层适应画布", + "desc": "缩放并调整视图以适应所有可见图层。" + }, + "deleteSelected": { + "title": "删除图层", + "desc": "删除选定的图层。" + } + }, + "hotkeys": "快捷键", + "workflows": { + "pasteSelection": { + "title": "粘贴", + "desc": "粘贴复制的节点和边。" + }, + "title": "工作流", + "addNode": { + "title": "添加节点", + "desc": "打开添加节点菜单。" + }, + "copySelection": { + "desc": "复制选定的节点和边。", + "title": "复制" + }, + "pasteSelectionWithEdges": { + "title": "带边缘的粘贴", + "desc": "粘贴复制的节点、边,以及与复制的节点连接的所有边。" + }, + "selectAll": { + "title": "全选", + "desc": "选择所有节点和边。" + }, + "deleteSelection": { + "title": "删除", + "desc": "删除选定的节点和边。" + }, + "undo": { + "title": "撤销", + "desc": "撤销上一个工作流操作。" + }, + "redo": { + "desc": "重做上一个工作流操作。", + "title": "重做" + } + }, + "gallery": { + "title": "画廊", + "galleryNavUp": { + "title": "向上导航", + "desc": "在图库网格中向上导航,选择该图像。如果在页面顶部,则转到上一页。" + }, + "galleryNavUpAlt": { + "title": "向上导航(比较图像)", + "desc": "与向上导航相同,但选择比较图像,如果比较模式尚未打开,则将其打开。" + }, + "selectAllOnPage": { + "desc": "选择当前页面上的所有图像。", + "title": "选页面上的所有内容" + }, + "galleryNavDownAlt": { + "title": "向下导航(比较图像)", + "desc": "与向下导航相同,但选择比较图像,如果比较模式尚未打开,则将其打开。" + }, + "galleryNavLeftAlt": { + "title": "向左导航(比较图像)", + "desc": "与向左导航相同,但选择比较图像,如果比较模式尚未打开,则将其打开。" + }, + "clearSelection": { + "title": "清除选择", + "desc": "清除当前的选择(如果有的话)。" + }, + "deleteSelection": { + "title": "删除", + "desc": "删除所有选定的图像。默认情况下,系统会提示您确认删除。如果这些图像当前在应用中使用,系统将发出警告。" + }, + "galleryNavLeft": { + "title": "向左导航", + "desc": "在图库网格中向左导航,选择该图像。如果处于行的第一张图像,转到上一行。如果处于页面的第一张图像,转到上一页。" + }, + "galleryNavRight": { + "title": "向右导航", + "desc": "在图库网格中向右导航,选择该图像。如果在行的最后一张图像,转到下一行。如果在页面的最后一张图像,转到下一页。" + }, + "galleryNavDown": { + "desc": "在图库网格中向下导航,选择该图像。如果在页面底部,则转到下一页。", + "title": "向下导航" + }, + "galleryNavRightAlt": { + "title": "向右导航(比较图像)", + "desc": "与向右导航相同,但选择比较图像,如果比较模式尚未打开,则将其打开。" + } + }, + "viewer": { + "toggleMetadata": { + "desc": "显示或隐藏当前图像的元数据覆盖。", + "title": "显示/隐藏元数据" + }, + "recallPrompts": { + "desc": "召回当前图像的正面和负面提示。", + "title": "召回提示" + }, + "toggleViewer": { + "title": "显示/隐藏图像查看器", + "desc": "显示或隐藏图像查看器。仅在画布选项卡上可用。" + }, + "recallAll": { + "desc": "召回当前图像的所有元数据。", + "title": "召回所有元数据" + }, + "recallSeed": { + "title": "召回种子", + "desc": "召回当前图像的种子。" + }, + "swapImages": { + "title": "交换比较图像", + "desc": "交换正在比较的图像。" + }, + "nextComparisonMode": { + "title": "下一个比较模式", + "desc": "环浏览比较模式。" + }, + "loadWorkflow": { + "title": "加载工作流", + "desc": "加载当前图像的保存工作流程(如果有的话)。" + }, + "title": "图像查看器", + "remix": { + "title": "混合", + "desc": "召回当前图像的所有元数据,除了种子。" + }, + "useSize": { + "title": "使用尺寸", + "desc": "使用当前图像的尺寸作为边界框尺寸。" + }, + "runPostprocessing": { + "title": "行后处理", + "desc": "对当前图像运行所选的后处理。" + } + } + }, + "modelManager": { + "modelManager": "模型管理器", + "model": "模型", + "modelUpdated": "模型已更新", + "manual": "手动", + "name": "名称", + "description": "描述", + "config": "配置", + "width": "宽度", + "height": "高度", + "addModel": "添加模型", + "availableModels": "可用模型", + "search": "检索", + "load": "加载", + "active": "活跃", + "selected": "已选择", + "delete": "删除", + "deleteModel": "删除模型", + "deleteConfig": "删除配置", + "deleteMsg1": "您确定要将该模型从 InvokeAI 删除吗?", + "deleteMsg2": "磁盘中放置在 InvokeAI 根文件夹的 checkpoint 文件会被删除。若你正在使用自定义目录,则不会从磁盘中删除他们。", + "convertToDiffusersHelpText1": "模型会被转换成 🧨 Diffusers 格式。", + "convertToDiffusersHelpText2": "这个过程会替换你的模型管理器的入口中相同 Diffusers 版本的模型。", + "convertToDiffusersHelpText4": "这是一次性的处理过程。根据你电脑的配置不同耗时 30 - 60 秒。", + "convertToDiffusersHelpText6": "你希望转换这个模型吗?", + "allModels": "全部模型", + "convertToDiffusers": "转换为 Diffusers", + "repo_id": "项目 ID", + "modelConverted": "模型已转换", + "convertToDiffusersHelpText3": "磁盘中放置在 InvokeAI 根文件夹的 checkpoint 文件会被删除. 若位于自定义目录, 则不会受影响.", + "convertToDiffusersHelpText5": "请确认你有足够的磁盘空间,模型大小通常在 2 GB - 7 GB 之间。", + "convert": "转换", + "none": "无", + "modelDeleteFailed": "模型删除失败", + "selectModel": "选择模型", + "settings": "设置", + "syncModels": "同步模型", + "modelDeleted": "模型已删除", + "modelUpdateFailed": "模型更新失败", + "modelConversionFailed": "模型转换失败", + "baseModel": "基底模型", + "convertingModelBegin": "模型转换中. 请稍候.", + "predictionType": "预测类型", + "advanced": "高级", + "modelType": "模型类别", + "variant": "变体", + "vae": "VAE", + "alpha": "Alpha", + "vaePrecision": "VAE 精度", + "noModelSelected": "无选中的模型", + "modelImageUpdateFailed": "模型图像更新失败", + "scanFolder": "扫描文件夹", + "path": "路径", + "pathToConfig": "配置路径", + "cancel": "取消", + "install": "安装", + "simpleModelPlaceholder": "本地文件或diffusers文件夹的URL或路径", + "noModelsInstalledDesc1": "安装模型时使用", + "inplaceInstallDesc": "安装模型时,不复制文件,直接从原位置加载。如果关闭此选项,模型文件将在安装过程中被复制到Invoke管理的模型文件夹中.", + "installAll": "安装全部", + "noModelsInstalled": "无已安装的模型", + "urlOrLocalPathHelper": "链接应该指向单个文件.本地路径可以指向单个文件,或者对于单个扩散模型(diffusers model),可以指向一个文件夹.", + "modelSettings": "模型设置", + "scanPlaceholder": "本地文件夹路径", + "installRepo": "安装仓库", + "modelImageDeleted": "模型图像已删除", + "modelImageDeleteFailed": "模型图像删除失败", + "scanFolderHelper": "此文件夹将进行递归扫描以寻找模型.对于大型文件夹,这可能需要一些时间.", + "scanResults": "扫描结果", + "noMatchingModels": "无匹配的模型", + "pruneTooltip": "清理队列中已完成的导入任务", + "urlOrLocalPath": "链接或本地路径", + "localOnly": "仅本地", + "huggingFaceHelper": "如果在此代码库中检测到多个模型,系统将提示您选择其中一个进行安装.", + "imageEncoderModelId": "图像编码器模型ID", + "modelImageUpdated": "模型图像已更新", + "modelName": "模型名称", + "prune": "清理", + "repoVariant": "代码库版本", + "defaultSettings": "默认设置", + "inplaceInstall": "就地安装", + "main": "主界面", + "starterModels": "初始模型", + "installQueue": "安装队列", + "mainModelTriggerPhrases": "主模型触发词", + "typePhraseHere": "在此输入触发词", + "triggerPhrases": "触发词", + "metadata": "元数据", + "deleteModelImage": "删除模型图片", + "edit": "编辑", + "source": "来源", + "uploadImage": "上传图像", + "addModels": "添加模型", + "textualInversions": "文本逆向生成", + "upcastAttention": "是否为高精度权重", + "defaultSettingsSaved": "默认设置已保存", + "huggingFacePlaceholder": "所有者或模型名称", + "huggingFaceRepoID": "HuggingFace仓库ID", + "loraTriggerPhrases": "LoRA 触发词", + "spandrelImageToImage": "图生图(Spandrel)", + "noDefaultSettings": "此模型没有配置默认设置。请访问模型管理器添加默认设置。", + "clipEmbed": "CLIP 嵌入", + "defaultSettingsOutOfSync": "某些设置与模型的默认值不匹配:", + "restoreDefaultSettings": "点击以使用模型的默认设置。", + "usingDefaultSettings": "使用模型的默认设置", + "huggingFace": "HuggingFace", + "hfTokenInvalid": "HF 令牌无效或缺失", + "hfTokenLabel": "HuggingFace 令牌(某些模型所需)", + "hfTokenHelperText": "使用某些模型需要 HF 令牌。点击这里创建或获取你的令牌。", + "includesNModels": "包括 {{n}} 个模型及其依赖项", + "starterBundles": "启动器包", + "learnMoreAboutSupportedModels": "了解更多关于我们支持的模型的信息", + "hfForbidden": "您没有权限访问这个 HF 模型", + "hfTokenInvalidErrorMessage": "无效或缺失 HuggingFace 令牌。", + "hfTokenRequired": "您正在尝试下载一个需要有效 HuggingFace 令牌的模型。", + "hfTokenSaved": "HF 令牌已保存", + "hfForbiddenErrorMessage": "我们建议访问 HuggingFace.com 上的仓库页面。所有者可能要求您接受条款才能下载。", + "hfTokenUnableToVerifyErrorMessage": "无法验证 HuggingFace 令牌。这可能是由于网络错误导致的。请稍后再试。", + "hfTokenInvalidErrorMessage2": "在这里更新它。 ", + "hfTokenUnableToVerify": "无法验证 HF 令牌", + "skippingXDuplicates_other": "跳过 {{count}} 个重复项", + "starterBundleHelpText": "轻松安装所有用于启动基础模型所需的模型,包括主模型、ControlNets、IP适配器等。选择一个安装包时,会跳过已安装的模型。", + "installingBundle": "正在安装模型包", + "installingModel": "正在安装模型", + "installingXModels_other": "正在安装 {{count}} 个模型", + "t5Encoder": "T5 编码器", + "clipLEmbed": "CLIP-L 嵌入", + "clipGEmbed": "CLIP-G 嵌入", + "loraModels": "LoRAs(低秩适配)" + }, + "parameters": { + "images": "图像", + "steps": "步数", + "cfgScale": "CFG 等级", + "width": "宽度", + "height": "高度", + "seed": "种子", + "shuffle": "随机生成种子", + "noiseThreshold": "噪声阈值", + "perlinNoise": "Perlin 噪声", + "type": "种类", + "strength": "强度", + "upscaling": "放大", + "scale": "等级", + "imageFit": "使生成图像长宽适配初始图像", + "scaleBeforeProcessing": "处理前缩放", + "scaledWidth": "缩放宽度", + "scaledHeight": "缩放长度", + "infillMethod": "填充方法", + "tileSize": "方格尺寸", + "usePrompt": "使用提示", + "useSeed": "使用种子", + "useAll": "使用所有参数", + "info": "信息", + "seamlessYAxis": "无缝平铺 Y 轴", + "seamlessXAxis": "无缝平铺 X 轴", + "denoisingStrength": "去噪强度", + "cancel": { + "cancel": "取消" + }, + "copyImage": "复制图片", + "symmetry": "对称性", + "positivePromptPlaceholder": "正向提示词", + "negativePromptPlaceholder": "负向提示词", + "scheduler": "调度器", + "general": "通用", + "controlNetControlMode": "控制模式", + "maskBlur": "遮罩模糊", + "invoke": { + "noNodesInGraph": "节点图中无节点", + "noModelSelected": "无已选中的模型", + "invoke": "调用", + "missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} 缺失输入", + "systemDisconnected": "系统已断开连接", + "missingNodeTemplate": "缺失节点模板", + "missingFieldTemplate": "缺失模板", + "addingImagesTo": "添加图像到", + "noPrompts": "没有已生成的提示词", + "canvasIsFiltering": "画布正在过滤", + "noCLIPEmbedModelSelected": "未为FLUX生成选择CLIP嵌入模型", + "noFLUXVAEModelSelected": "未为FLUX生成选择VAE模型", + "canvasIsRasterizing": "画布正在栅格化", + "canvasIsCompositing": "画布正在合成", + "noT5EncoderModelSelected": "未为FLUX生成选择T5编码器模型", + "canvasIsTransforming": "画布正在变换" + }, + "patchmatchDownScaleSize": "缩小", + "clipSkip": "CLIP 跳过层", + "useCpuNoise": "使用 CPU 噪声", + "coherenceMode": "模式", + "imageActions": "图像操作", + "iterations": "迭代数", + "cfgRescaleMultiplier": "CFG 重缩放倍数", + "useSize": "使用尺寸", + "setToOptimalSize": "优化模型大小", + "setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (可能过小)", + "lockAspectRatio": "锁定纵横比", + "swapDimensions": "交换尺寸", + "aspect": "纵横", + "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (可能过大)", + "remixImage": "重新混合图像", + "coherenceEdgeSize": "边缘尺寸", + "postProcessing": "后处理(Shift + U)", + "sendToUpscale": "发送到放大", + "processImage": "处理图像", + "infillColorValue": "填充颜色", + "coherenceMinDenoise": "最小去噪", + "sendToCanvas": "发送到画布", + "disabledNoRasterContent": "已禁用(无栅格内容)", + "optimizedImageToImage": "优化的图生图", + "guidance": "引导", + "gaussianBlur": "高斯模糊", + "recallMetadata": "调用元数据", + "boxBlur": "方框模糊", + "staged": "已分阶段处理" + }, + "settings": { + "models": "模型", + "displayInProgress": "显示处理中的图像", + "confirmOnDelete": "删除时确认", + "resetWebUI": "重置网页界面", + "resetWebUIDesc1": "重置网页只会重置浏览器中缓存的图像和设置,不会删除任何图像。", + "resetWebUIDesc2": "如果图像没有显示在图库中,或者其他东西不工作,请在GitHub上提交问题之前尝试重置。", + "resetComplete": "网页界面已重置。", + "showProgressInViewer": "在查看器中展示过程图片", + "antialiasProgressImages": "对过程图像应用抗锯齿", + "generation": "生成", + "ui": "用户界面", + "general": "通用", + "developer": "开发者", + "beta": "Beta", + "clearIntermediates": "清除中间产物", + "clearIntermediatesDesc3": "您图库中的图像不会被删除。", + "clearIntermediatesDesc2": "中间产物图像是生成过程中产生的副产品,与图库中的结果图像不同。清除中间产物可释放磁盘空间。", + "intermediatesCleared_other": "已清除 {{count}} 个中间产物", + "clearIntermediatesDesc1": "清除中间产物会重置您的画布和 ControlNet 状态。", + "intermediatesClearedFailed": "清除中间产物时出现问题", + "clearIntermediatesWithCount_other": "清除 {{count}} 个中间产物", + "clearIntermediatesDisabled": "队列为空才能清理中间产物", + "enableNSFWChecker": "启用成人内容检测器", + "enableInvisibleWatermark": "启用不可见水印", + "enableInformationalPopovers": "启用信息弹窗", + "reloadingIn": "重新加载中", + "informationalPopoversDisabled": "信息提示框已禁用", + "informationalPopoversDisabledDesc": "信息提示框已被禁用.请在设置中重新启用.", + "enableModelDescriptions": "在下拉菜单中启用模型描述", + "confirmOnNewSession": "新会话时确认", + "showDetailedInvocationProgress": "显示进度详情" + }, + "toast": { + "uploadFailed": "上传失败", + "imageCopied": "图像已复制", + "parametersNotSet": "参数未恢复", + "uploadFailedInvalidUploadDesc": "必须是单个 PNG 或 JPEG 图像。", + "connected": "服务器连接", + "parameterSet": "参数已恢复", + "parameterNotSet": "参数未恢复", + "serverError": "服务器错误", + "canceled": "处理取消", + "problemCopyingImage": "无法复制图像", + "modelAddedSimple": "模型已加入队列", + "loadedWithWarnings": "已加载带有警告的工作流", + "imageUploaded": "图像已上传", + "addedToBoard": "添加到{{name}}的资产中", + "workflowLoaded": "工作流已加载", + "imageUploadFailed": "图像上传失败", + "baseModelChangedCleared_other": "已清除或禁用{{count}}个不兼容的子模型", + "problemDeletingWorkflow": "删除工作流时出现问题", + "workflowDeleted": "已删除工作流", + "problemRetrievingWorkflow": "检索工作流时发生问题", + "baseModelChanged": "基础模型已更改", + "problemDownloadingImage": "无法下载图像", + "outOfMemoryError": "内存不足错误", + "parameters": "参数", + "parameterNotSetDescWithMessage": "无法恢复 {{parameter}}: {{message}}", + "parameterSetDesc": "已恢复 {{parameter}}", + "parameterNotSetDesc": "无法恢复{{parameter}}", + "sessionRef": "会话: {{sessionId}}", + "somethingWentWrong": "出现错误", + "prunedQueue": "已清理队列", + "outOfMemoryErrorDesc": "您当前的生成设置已超出系统处理能力.请调整设置后再次尝试.", + "parametersSet": "参数已恢复", + "errorCopied": "错误信息已复制", + "modelImportCanceled": "模型导入已取消", + "importFailed": "导入失败", + "importSuccessful": "导入成功", + "sentToUpscale": "已发送到放大处理", + "addedToUncategorized": "已添加到看板 $t(boards.uncategorized) 的资产中", + "linkCopied": "链接已复制", + "problemSavingLayer": "无法保存图层", + "unableToLoadImage": "无法加载图像", + "unableToLoadStylePreset": "无法加载样式预设", + "stylePresetLoaded": "样式预设已加载", + "problemCopyingLayer": "无法复制图层", + "sentToCanvas": "已发送到画布", + "unableToLoadImageMetadata": "无法加载图像元数据", + "layerCopiedToClipboard": "图层已复制到剪贴板", + "imagesWillBeAddedTo": "上传的图像将添加到看板 {{boardName}} 的资产中。" + }, + "accessibility": { + "invokeProgressBar": "Invoke 进度条", + "reset": "重置", + "nextImage": "下一张图片", + "uploadImage": "上传图片", + "previousImage": "上一张图片", + "menu": "菜单", + "mode": "模式", + "resetUI": "$t(accessibility.reset) UI", + "createIssue": "创建问题", + "about": "关于", + "submitSupportTicket": "提交支持工单", + "toggleRightPanel": "切换右侧面板(G)", + "uploadImages": "上传图片", + "toggleLeftPanel": "开关左侧面板(T)" + }, + "nodes": { + "zoomInNodes": "放大", + "loadWorkflow": "加载工作流", + "zoomOutNodes": "缩小", + "reloadNodeTemplates": "重载节点模板", + "fitViewportNodes": "自适应视图", + "showMinimapnodes": "显示缩略图", + "hideMinimapnodes": "隐藏缩略图", + "downloadWorkflow": "下载工作流 JSON", + "workflowDescription": "简述", + "noNodeSelected": "无选中的节点", + "addNode": "添加节点", + "unableToValidateWorkflow": "无法验证工作流", + "noOutputRecorded": "无已记录输出", + "updateApp": "升级 App", + "colorCodeEdgesHelp": "根据连接区域对边缘编码颜色", + "workflowContact": "联系", + "animatedEdges": "边缘动效", + "nodeTemplate": "节点模板", + "snapToGrid": "对齐网格", + "nodeSearch": "检索节点", + "version": "版本", + "validateConnections": "验证连接和节点图", + "inputMayOnlyHaveOneConnection": "输入仅能有一个连接", + "notes": "注释", + "nodeOutputs": "节点输出", + "currentImageDescription": "在节点编辑器中显示当前图像", + "validateConnectionsHelp": "防止建立无效连接和调用无效节点图", + "problemSettingTitle": "设定标题时出现问题", + "noConnectionInProgress": "没有正在进行的连接", + "workflowVersion": "版本", + "fieldTypesMustMatch": "类型必须匹配", + "workflow": "工作流", + "animatedEdgesHelp": "为选中边缘和其连接的选中节点的边缘添加动画", + "workflowTags": "标签", + "fullyContainNodesHelp": "节点必须完全位于选择框中才能被选中", + "workflowValidation": "工作流验证错误", + "executionStateInProgress": "处理中", + "executionStateError": "错误", + "executionStateCompleted": "已完成", + "workflowAuthor": "作者", + "currentImage": "当前图像", + "workflowName": "名称", + "cannotConnectInputToInput": "无法将输入连接到输入", + "workflowNotes": "注释", + "cannotConnectOutputToOutput": "无法将输出连接到输出", + "connectionWouldCreateCycle": "连接将创建一个循环", + "cannotConnectToSelf": "无法连接自己", + "notesDescription": "添加有关您的工作流的注释", + "unknownField": "未知", + "colorCodeEdges": "边缘颜色编码", + "unknownNode": "未知节点", + "addNodeToolTip": "添加节点 (Shift+A, Space)", + "loadingNodes": "加载节点中...", + "snapToGridHelp": "移动时将节点与网格对齐", + "workflowSettings": "工作流编辑器设置", + "scheduler": "调度器", + "missingTemplate": "无效的节点:类型为 {{type}} 的节点 {{node}} 缺失模板(无已安装模板?)", + "nodeOpacity": "节点不透明度", + "updateNode": "更新节点", + "edge": "边缘", + "noWorkflow": "无工作流", + "nodeType": "节点类型", + "fullyContainNodes": "完全包含节点来进行选择", + "node": "节点", + "collection": "合集", + "string": "字符串", + "cannotDuplicateConnection": "无法创建重复的连接", + "enum": "Enum (枚举)", + "float": "浮点", + "integer": "整数", + "boolean": "布尔值", + "ipAdapter": "IP-Adapter", + "updateAllNodes": "更新节点", + "unableToUpdateNodes_other": "{{count}} 个节点无法完成更新", + "inputFieldTypeParseError": "无法解析 {{node}} 的输入类型 {{field}}。({{message}})", + "unsupportedArrayItemType": "不支持的数组类型 \"{{type}}\"", + "targetNodeFieldDoesNotExist": "无效的边缘:{{node}} 的目标/输入区域 {{field}} 不存在", + "unsupportedMismatchedUnion": "合集或标量类型与基类 {{firstType}} 和 {{secondType}} 不匹配", + "allNodesUpdated": "已更新所有节点", + "sourceNodeDoesNotExist": "无效的边缘:{{node}} 的源/输出节点不存在", + "unableToExtractEnumOptions": "无法提取枚举选项", + "unableToParseFieldType": "无法解析类型", + "outputFieldTypeParseError": "无法解析 {{node}} 的输出类型 {{field}}。({{message}})", + "sourceNodeFieldDoesNotExist": "无效的边缘:{{node}} 的源/输出区域 {{field}} 不存在", + "unableToGetWorkflowVersion": "无法获取工作流架构版本", + "nodePack": "节点包", + "unableToExtractSchemaNameFromRef": "无法从参考中提取架构名", + "unknownErrorValidatingWorkflow": "验证工作流时出现未知错误", + "collectionFieldType": "{{name}}(合集)", + "unknownNodeType": "未知节点类型", + "targetNodeDoesNotExist": "无效的边缘:{{node}} 的目标/输入节点不存在", + "unknownFieldType": "$t(nodes.unknownField) 类型:{{type}}", + "collectionOrScalarFieldType": "{{name}} (单一项目或项目集合)", + "nodeVersion": "节点版本", + "deletedInvalidEdge": "已删除无效的边缘 {{source}} -> {{target}}", + "prototypeDesc": "此调用是一个原型 (prototype)。它可能会在本项目更新期间发生破坏性更改,并且随时可能被删除。", + "betaDesc": "此调用尚处于测试阶段。在稳定之前,它可能会在项目更新期间发生破坏性更改。本项目计划长期支持这种调用。", + "newWorkflow": "新建工作流", + "newWorkflowDesc": "是否创建一个新的工作流?", + "newWorkflowDesc2": "当前工作流有未保存的更改。", + "unsupportedAnyOfLength": "联合(union)数据类型数目过多 ({{count}})", + "resetToDefaultValue": "重置为默认值", + "clearWorkflowDesc2": "您当前的工作流有未保存的更改.", + "missingNode": "缺少调用节点", + "missingInvocationTemplate": "缺少调用模版", + "noFieldsViewMode": "此工作流程未选择任何要显示的字段.请查看完整工作流程以进行配置.", + "viewMode": "在线性视图中使用", + "showEdgeLabelsHelp": "在边缘上显示标签,指示连接的节点", + "cannotMixAndMatchCollectionItemTypes": "集合项目类型不能混用", + "missingFieldTemplate": "缺少字段模板", + "editMode": "在工作流编辑器中编辑", + "showEdgeLabels": "显示边缘标签", + "clearWorkflowDesc": "是否清除当前工作流并创建新的?", + "graph": "图表", + "noGraph": "无图表", + "edit": "编辑", + "clearWorkflow": "清除工作流", + "imageAccessError": "无法找到图像 {{image_name}},正在恢复默认设置", + "boardAccessError": "无法找到面板 {{board_id}},正在恢复默认设置", + "modelAccessError": "无法找到模型 {{key}},正在恢复默认设置", + "noWorkflows": "无工作流程", + "workflowHelpText": "需要帮助?请查看我们的《工作流程入门指南》。", + "noMatchingWorkflows": "无匹配的工作流程", + "saveToGallery": "保存到图库", + "singleFieldType": "{{name}}(单一模型)" + }, + "queue": { + "status": "状态", + "cancelTooltip": "取消当前项目", + "queueEmpty": "队列为空", + "pauseSucceeded": "处理器已暂停", + "in_progress": "处理中", + "queueFront": "添加到队列前", + "completed": "已完成", + "queueBack": "添加到队列", + "cancelFailed": "取消项目时出现问题", + "pauseFailed": "暂停处理器时出现问题", + "clearFailed": "清除队列时出现问题", + "clearSucceeded": "队列已清除", + "pause": "暂停", + "cancelSucceeded": "项目已取消", + "queue": "队列", + "batch": "批处理", + "clearQueueAlertDialog": "清空队列将立即取消所有正在处理的项目,并完全清空队列。待处理的过滤器将被取消。", + "pending": "待定", + "completedIn": "完成于", + "resumeFailed": "恢复处理器时出现问题", + "clear": "清除", + "prune": "修剪", + "total": "总计", + "canceled": "已取消", + "pruneFailed": "修剪队列时出现问题", + "cancelBatchSucceeded": "批处理已取消", + "clearTooltip": "取消并清除所有项目", + "current": "当前", + "pauseTooltip": "暂停处理器", + "failed": "已失败", + "cancelItem": "取消项目", + "next": "下一个", + "cancelBatch": "取消批处理", + "cancel": "取消", + "resumeSucceeded": "处理器已恢复", + "resumeTooltip": "恢复处理器", + "resume": "恢复", + "cancelBatchFailed": "取消批处理时出现问题", + "clearQueueAlertDialog2": "您确定要清除队列吗?", + "item": "项目", + "pruneSucceeded": "从队列修剪 {{item_count}} 个已完成的项目", + "notReady": "无法排队", + "batchFailedToQueue": "批次加入队列失败", + "batchQueued": "加入队列的批次", + "front": "前", + "pruneTooltip": "修剪 {{item_count}} 个已完成的项目", + "batchQueuedDesc_other": "在队列的 {{direction}} 中添加了 {{count}} 个会话", + "graphQueued": "节点图已加入队列", + "back": "后", + "session": "会话", + "enqueueing": "队列中的批次", + "graphFailedToQueue": "节点图加入队列失败", + "time": "时间", + "openQueue": "打开队列", + "prompts_other": "提示词", + "iterations_other": "迭代", + "generations_other": "生成", + "canvas": "画布", + "workflows": "工作流", + "generation": "生成", + "other": "其他", + "gallery": "画廊", + "destination": "目标存储", + "upscaling": "高清放大", + "origin": "来源" + }, + "sdxl": { + "refinerStart": "Refiner 开始作用时机", + "scheduler": "调度器", + "cfgScale": "CFG 等级", + "noModelsAvailable": "无可用模型", + "negAestheticScore": "负向美学评分", + "denoisingStrength": "去噪强度", + "refinermodel": "Refiner 模型", + "posAestheticScore": "正向美学评分", + "loading": "加载中...", + "steps": "步数", + "refiner": "Refiner", + "refinerSteps": "精炼步数" + }, + "metadata": { + "positivePrompt": "正向提示词", + "negativePrompt": "负向提示词", + "generationMode": "生成模式", + "Threshold": "噪声阈值", + "metadata": "元数据", + "strength": "图生图强度", + "seed": "种子", + "imageDetails": "图像详细信息", + "model": "模型", + "noImageDetails": "未找到图像详细信息", + "cfgScale": "CFG 等级", + "height": "高度", + "noMetaData": "未找到元数据", + "width": "宽度", + "createdBy": "创建者是", + "workflow": "工作流", + "steps": "步数", + "scheduler": "调度器", + "recallParameters": "召回参数", + "noRecallParameters": "未找到要召回的参数", + "vae": "VAE", + "cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)", + "allPrompts": "所有提示", + "imageDimensions": "图像尺寸", + "parameterSet": "已设置参数{{parameter}}", + "guidance": "指导", + "seamlessXAxis": "无缝 X 轴", + "seamlessYAxis": "无缝 Y 轴", + "canvasV2Metadata": "画布" + }, + "models": { + "noMatchingModels": "无相匹配的模型", + "loading": "加载中", + "noModelsAvailable": "无可用模型", + "selectModel": "选择一个模型", + "noRefinerModelsInstalled": "无已安装的 SDXL Refiner 模型", + "addLora": "添加 LoRA", + "lora": "LoRA", + "defaultVAE": "默认 VAE", + "concepts": "概念" + }, + "boards": { + "autoAddBoard": "自动添加面板", + "topMessage": "该面板包含的图像正使用以下功能:", + "move": "移动", + "menuItemAutoAdd": "自动添加到该面板", + "myBoard": "我的面板", + "searchBoard": "检索面板...", + "noMatching": "没有相匹配的面板", + "selectBoard": "选择一个面板", + "cancel": "取消", + "addBoard": "添加面板", + "bottomMessage": "删除该面板并且将其对应的图像将重置当前使用该面板的所有功能。", + "uncategorized": "未分类", + "changeBoard": "更改面板", + "loading": "加载中...", + "clearSearch": "清除检索", + "downloadBoard": "下载面板", + "deleteBoardOnly": "仅删除面板", + "deleteBoard": "删除面板", + "deleteBoardAndImages": "删除面板和图像", + "deletedBoardsCannotbeRestored": "删除的面板无法恢复。选择“仅删除面板”选项后,相关图片将会被移至未分类区域。", + "movingImagesToBoard_other": "移动 {{count}} 张图像到面板:", + "selectedForAutoAdd": "已选中自动添加", + "noBoards": "没有{{boardType}}类型的面板", + "unarchiveBoard": "恢复面板", + "addPrivateBoard": "创建私密面板", + "addSharedBoard": "创建共享面板", + "boards": "面板", + "imagesWithCount_other": "{{count}}张图片", + "deletedPrivateBoardsCannotbeRestored": "删除的面板无法恢复。选择“仅删除面板”后,相关图片将会被移至图片创建者的私密未分类区域。", + "private": "私密面板", + "shared": "共享面板", + "archiveBoard": "归档面板", + "archived": "已归档", + "assetsWithCount_other": "{{count}}项资源", + "updateBoardError": "更新画板出错" + }, + "dynamicPrompts": { + "seedBehaviour": { + "perPromptDesc": "每次生成图像使用不同的种子", + "perIterationLabel": "每次迭代的种子", + "perIterationDesc": "每次迭代使用不同的种子", + "perPromptLabel": "每张图像的种子", + "label": "种子行为" + }, + "maxPrompts": "最大提示词数", + "dynamicPrompts": "动态提示词", + "promptsPreview": "提示词预览", + "showDynamicPrompts": "显示动态提示词", + "loading": "生成动态提示词中..." + }, + "popovers": { + "compositingMaskAdjustments": { + "heading": "遮罩调整", + "paragraphs": [ + "调整遮罩。" + ] + }, + "paramRatio": { + "heading": "纵横比", + "paragraphs": [ + "生成图像的尺寸纵横比。", + "图像尺寸(单位:像素)建议 SD 1.5 模型使用等效 512x512 的尺寸,SDXL 模型使用等效 1024x1024 的尺寸。" + ] + }, + "noiseUseCPU": { + "heading": "使用 CPU 噪声", + "paragraphs": [ + "选择由 CPU 或 GPU 生成噪声。", + "启用 CPU 噪声后,特定的种子将会在不同的设备上产生下相同的图像。", + "启用 CPU 噪声不会对性能造成影响。" + ] + }, + "paramVAEPrecision": { + "heading": "VAE 精度", + "paragraphs": [ + "在VAE编码和解码过程中使用的精度.", + "Fp16/半精度更高效,但可能会造成图像的一些微小差异." + ] + }, + "compositingCoherenceMode": { + "heading": "模式", + "paragraphs": [ + "用于将新生成的遮罩区域与原图像融合的方法." + ] + }, + "controlNetResizeMode": { + "heading": "缩放模式", + "paragraphs": [ + "调整Control Adapter输入图像大小以适应输出图像尺寸的方法." + ] + }, + "clipSkip": { + "paragraphs": [ + "跳过CLIP模型的层数.", + "某些模型更适合结合CLIP Skip功能使用." + ], + "heading": "CLIP 跳过层" + }, + "paramModel": { + "heading": "模型", + "paragraphs": [ + "用于图像生成的模型.不同的模型经过训练,专门用于产生不同的美学效果和内容." + ] + }, + "paramIterations": { + "heading": "迭代数", + "paragraphs": [ + "生成图像的数量。", + "若启用动态提示词,每种提示词都会生成这么多次。" + ] + }, + "compositingCoherencePass": { + "heading": "一致性层", + "paragraphs": [ + "第二轮去噪有助于合成内补/外扩图像。" + ] + }, + "paramNegativeConditioning": { + "paragraphs": [ + "生成过程会避免生成负向提示词中的概念。使用此选项来使输出排除部分质量或对象。", + "支持 Compel 语法 和 embeddings。" + ], + "heading": "负向提示词" + }, + "compositingBlurMethod": { + "heading": "模糊方式", + "paragraphs": [ + "应用于遮罩区域的模糊方法。" + ] + }, + "paramScheduler": { + "heading": "调度器", + "paragraphs": [ + "生成过程中所使用的调度器.", + "每个调度器决定了在生成过程中如何逐步向图像添加噪声,或者如何根据模型的输出更新样本." + ] + }, + "controlNetWeight": { + "heading": "权重", + "paragraphs": [ + "Control Adapter的权重.权重越高,对最终图像的影响越大." + ] + }, + "paramCFGScale": { + "heading": "CFG 等级", + "paragraphs": [ + "控制提示对生成过程的影响程度.", + "较高的CFG比例值可能会导致生成结果过度饱和和扭曲. " + ] + }, + "paramSteps": { + "heading": "步数", + "paragraphs": [ + "每次生成迭代执行的步数。", + "通常情况下步数越多结果越好,但需要更多生成时间。" + ] + }, + "paramPositiveConditioning": { + "heading": "正向提示词", + "paragraphs": [ + "引导生成过程。您可以使用任何单词或短语。", + "Compel 语法、动态提示词语法和 embeddings。" + ] + }, + "lora": { + "heading": "LoRA", + "paragraphs": [ + "与基础模型结合使用的轻量级模型." + ] + }, + "infillMethod": { + "heading": "填充方法", + "paragraphs": [ + "在重绘过程中使用的填充方法." + ] + }, + "controlNetBeginEnd": { + "heading": "开始 / 结束步数百分比", + "paragraphs": [ + "去噪过程中将应用Control Adapter 的部分.", + "通常,在去噪过程初期应用的Control Adapters用于指导整体构图,而在后期应用的Control Adapters则用于调整细节。" + ] + }, + "scaleBeforeProcessing": { + "heading": "处理前缩放", + "paragraphs": [ + "\"自动\"选项会在图像生成之前将所选区域调整到最适合模型的大小.", + "\"手动\"选项允许您在图像生成之前自行选择所选区域的宽度和高度." + ] + }, + "paramDenoisingStrength": { + "heading": "去噪强度", + "paragraphs": [ + "为输入图像添加的噪声量。", + "输入 0 会导致结果图像和输入完全相同,输入 1 则会生成全新的图像。", + "当没有具有可见内容的栅格图层时,此设置将被忽略。" + ] + }, + "paramSeed": { + "heading": "种子", + "paragraphs": [ + "控制用于生成的起始噪声。", + "禁用\"随机\"选项,以使用相同的生成设置产生一致的结果." + ] + }, + "controlNetControlMode": { + "heading": "控制模式", + "paragraphs": [ + "在提示词和ControlNet之间分配更多的权重." + ] + }, + "dynamicPrompts": { + "paragraphs": [ + "动态提示词可将单个提示词解析为多个。", + "基本语法示例:\"a {red|green|blue} ball\"。这会产生三种提示词:\"a red ball\", \"a green ball\" 和 \"a blue ball\"。", + "可以在单个提示词中多次使用该语法,但务必请使用最大提示词设置来控制生成的提示词数量。" + ], + "heading": "动态提示词" + }, + "paramVAE": { + "paragraphs": [ + "用于将 AI 输出转换成最终图像的模型。" + ], + "heading": "VAE" + }, + "dynamicPromptsSeedBehaviour": { + "paragraphs": [ + "控制生成提示词时种子的使用方式。", + "每次迭代过程都会使用一个唯一的种子。使用本选项来探索单个种子的提示词变化。", + "例如,如果你有 5 种提示词,则生成的每个图像都会使用相同种子。", + "为每张图像使用独立的唯一种子。这可以提供更多变化。" + ], + "heading": "种子行为" + }, + "dynamicPromptsMaxPrompts": { + "heading": "最大提示词数量", + "paragraphs": [ + "限制动态提示词可生成的提示词数量。" + ] + }, + "controlNet": { + "paragraphs": [ + "ControlNet 为生成过程提供引导,为生成具有受控构图、结构、样式的图像提供帮助,具体的功能由所选的模型决定。" + ], + "heading": "ControlNet" + }, + "paramCFGRescaleMultiplier": { + "heading": "CFG 重缩放倍数", + "paragraphs": [ + "CFG指导的重缩放乘数,适用于使用零终端信噪比(ztsnr)训练的模型.", + "对于这些模型,建议的数值为0.7." + ] + }, + "imageFit": { + "paragraphs": [ + "将初始图像调整到与输出图像相同的宽度和高度.建议启用此功能." + ], + "heading": "将初始图像适配到输出大小" + }, + "paramAspect": { + "paragraphs": [ + "生成图像的宽高比.调整宽高比会相应地更新图像的宽度和高度.", + "选择\"优化\"将把图像的宽度和高度设置为所选模型的最优尺寸." + ], + "heading": "宽高比" + }, + "refinerSteps": { + "paragraphs": [ + "在图像生成过程中的细化阶段将执行的步骤数.", + "与生成步骤相似." + ], + "heading": "步数" + }, + "compositingMaskBlur": { + "heading": "遮罩模糊", + "paragraphs": [ + "遮罩的模糊范围." + ] + }, + "compositingCoherenceMinDenoise": { + "paragraphs": [ + "连贯模式下的最小去噪力度", + "在图像修复或重绘过程中,连贯区域的最小去噪力度" + ], + "heading": "最小去噪" + }, + "loraWeight": { + "paragraphs": [ + "LoRA的权重,权重越高对最终图像的影响越大." + ], + "heading": "权重" + }, + "paramHrf": { + "heading": "启用高分辨率修复", + "paragraphs": [ + "以高于模型最优分辨率的大分辨率生成高质量图像.这通常用于防止生成图像中出现重复内容." + ] + }, + "compositingCoherenceEdgeSize": { + "paragraphs": [ + "连贯处理的边缘尺寸." + ], + "heading": "边缘尺寸" + }, + "paramWidth": { + "paragraphs": [ + "生成图像的宽度.必须是8的倍数." + ], + "heading": "宽度" + }, + "refinerScheduler": { + "paragraphs": [ + "在图像生成过程中的细化阶段所使用的调度程序.", + "与生成调度程序相似." + ], + "heading": "调度器" + }, + "seamlessTilingXAxis": { + "paragraphs": [ + "沿水平轴将图像进行无缝平铺." + ], + "heading": "无缝平铺X轴" + }, + "paramUpscaleMethod": { + "heading": "放大方法", + "paragraphs": [ + "用于高分辨率修复的图像放大方法." + ] + }, + "refinerModel": { + "paragraphs": [ + "在图像生成过程中的细化阶段所使用的模型.", + "与生成模型相似." + ], + "heading": "精炼模型" + }, + "paramHeight": { + "paragraphs": [ + "生成图像的高度.必须是8的倍数." + ], + "heading": "高" + }, + "patchmatchDownScaleSize": { + "heading": "缩小", + "paragraphs": [ + "在填充之前图像缩小的程度.", + "较高的缩小比例会提升处理速度,但可能会降低图像质量." + ] + }, + "seamlessTilingYAxis": { + "heading": "Y轴上的无缝平铺", + "paragraphs": [ + "沿垂直轴将图像进行无缝平铺." + ] + }, + "ipAdapterMethod": { + "paragraphs": [ + "当前IP Adapter的应用方法." + ], + "heading": "方法" + }, + "controlNetProcessor": { + "paragraphs": [ + "处理输入图像以引导生成过程的方法.不同的处理器会在生成图像中产生不同的效果或风格." + ], + "heading": "处理器" + }, + "refinerPositiveAestheticScore": { + "paragraphs": [ + "根据训练数据,对生成结果进行加权,使其更接近于具有高美学评分的图像." + ], + "heading": "正面美学评分" + }, + "refinerStart": { + "paragraphs": [ + "在图像生成过程中精炼阶段开始被使用的时刻.", + "0表示精炼器将全程参与图像生成,0.8表示细化器仅在生成过程的最后20%阶段被使用." + ], + "heading": "精炼开始" + }, + "refinerCfgScale": { + "paragraphs": [ + "控制提示对生成过程的影响程度.", + "与生成CFG Scale相似." + ], + "heading": "CFG比例" + }, + "structure": { + "heading": "结构", + "paragraphs": [ + "结构决定了输出图像在多大程度上保持原始图像的布局.较低的结构设置允许进行较大的变化,而较高的结构设置则会严格保持原始图像的构图和布局." + ] + }, + "creativity": { + "paragraphs": [ + "创造力决定了模型在添加细节时的自由度.较低的创造力会使生成结果更接近原始图像,而较高的创造力则允许更多的变化.在使用提示时,较高的创造力会增加提示对生成结果的影响." + ], + "heading": "创造力" + }, + "refinerNegativeAestheticScore": { + "paragraphs": [ + "根据训练数据,对生成结果进行加权,使其更接近于具有低美学评分的图像." + ], + "heading": "负面美学评分" + }, + "upscaleModel": { + "heading": "放大模型", + "paragraphs": [ + "上采样模型在添加细节之前将图像放大到输出尺寸.虽然可以使用任何支持的上采样模型,但有些模型更适合处理特定类型的图像,例如照片或线条画." + ] + }, + "scale": { + "heading": "缩放", + "paragraphs": [ + "比例控制决定了输出图像的大小,它是基于输入图像分辨率的倍数来计算的.例如对一张1024x1024的图像进行2倍上采样,将会得到一张2048x2048的输出图像." + ] + }, + "globalReferenceImage": { + "heading": "全局参考图像", + "paragraphs": [ + "应用参考图像以影响整个生成过程。" + ] + }, + "rasterLayer": { + "paragraphs": [ + "画布的基于像素的内容,用于图像生成过程。" + ], + "heading": "栅格图层" + }, + "regionalGuidanceAndReferenceImage": { + "paragraphs": [ + "对于区域引导,使用画笔引导全局提示中的元素应出现的位置。", + "对于区域参考图像,使用画笔将参考图像应用到特定区域。" + ], + "heading": "区域引导与区域参考图像" + }, + "regionalReferenceImage": { + "heading": "区域参考图像", + "paragraphs": [ + "使用画笔将参考图像应用到特定区域。" + ] + }, + "optimizedDenoising": { + "heading": "优化的图生图", + "paragraphs": [ + "启用‘优化的图生图’功能,可在使用 Flux 模型进行图生图和图像修复转换时提供更平滑的降噪强度调节。此设置可以提高对图像变化程度的控制能力,但如果您更倾向于使用标准的降噪强度调节方式,也可以关闭此功能。该设置仍在优化中,目前处于测试阶段。" + ] + }, + "inpainting": { + "paragraphs": [ + "控制由降噪强度引导的修改区域。" + ], + "heading": "图像重绘" + }, + "regionalGuidance": { + "heading": "区域引导", + "paragraphs": [ + "使用画笔引导全局提示中的元素应出现的位置。" + ] + }, + "fluxDevLicense": { + "heading": "非商业许可", + "paragraphs": [ + "FLUX.1 [dev] 模型受 FLUX [dev] 非商业许可协议的约束。如需在 Invoke 中将此模型类型用于商业目的,请访问我们的网站了解更多信息。" + ] + }, + "paramGuidance": { + "paragraphs": [ + "控制提示对生成过程的影响程度。", + "较高的引导值可能导致过度饱和,而过高或过低的引导值可能导致生成结果失真。引导仅适用于FLUX DEV模型。" + ], + "heading": "引导" + } + }, + "invocationCache": { + "disable": "禁用", + "misses": "缓存未中", + "enableFailed": "启用调用缓存时出现问题", + "invocationCache": "调用缓存", + "clearSucceeded": "调用缓存已清除", + "enableSucceeded": "调用缓存已启用", + "clearFailed": "清除调用缓存时出现问题", + "hits": "缓存命中", + "disableSucceeded": "调用缓存已禁用", + "disableFailed": "禁用调用缓存时出现问题", + "enable": "启用", + "clear": "清除", + "maxCacheSize": "最大缓存大小", + "cacheSize": "缓存大小", + "useCache": "使用缓存" + }, + "hrf": { + "metadata": { + "strength": "高分辨率修复强度", + "enabled": "高分辨率修复已启用", + "method": "高分辨率修复方法" + }, + "hrf": "高分辨率修复" + }, + "workflows": { + "saveWorkflowAs": "保存工作流为", + "workflowEditorMenu": "工作流编辑器菜单", + "workflowName": "工作流名称", + "saveWorkflow": "保存工作流", + "workflowLibrary": "工作流库", + "downloadWorkflow": "保存到文件", + "workflowSaved": "已保存工作流", + "unnamedWorkflow": "未命名的工作流", + "savingWorkflow": "保存工作流中...", + "loading": "加载工作流中", + "problemSavingWorkflow": "保存工作流时出现问题", + "deleteWorkflow": "删除工作流", + "workflows": "工作流", + "uploadWorkflow": "从文件中加载", + "newWorkflowCreated": "已创建新的工作流", + "name": "名称", + "created": "已创建", + "ascending": "升序", + "descending": "降序", + "updated": "已更新", + "opened": "已打开", + "workflowCleared": "工作流已清除", + "saveWorkflowToProject": "保存工作流到项目", + "noWorkflows": "无工作流", + "convertGraph": "转换图表", + "loadWorkflow": "$t(common.load) 工作流", + "loadFromGraph": "从图表加载工作流", + "autoLayout": "自动布局", + "edit": "编辑", + "copyShareLinkForWorkflow": "复制工作流程的分享链接", + "delete": "删除", + "download": "下载", + "copyShareLink": "复制分享链接", + "chooseWorkflowFromLibrary": "从库中选择工作流程", + "deleteWorkflow2": "您确定要删除此工作流程吗?此操作无法撤销。" + }, + "accordions": { + "compositing": { + "infillTab": "内补", + "coherenceTab": "一致性层", + "title": "合成" + }, + "control": { + "title": "Control" + }, + "generation": { + "title": "生成" + }, + "advanced": { + "title": "高级", + "options": "$t(accordions.advanced.title) 选项" + }, + "image": { + "title": "图像" + } + }, + "prompt": { + "addPromptTrigger": "添加提示词触发器", + "noMatchingTriggers": "没有匹配的触发器", + "compatibleEmbeddings": "兼容的嵌入" + }, + "controlLayers": { + "autoNegative": "自动反向", + "moveForward": "向前移动", + "moveBackward": "向后移动", + "regionalGuidance": "区域导向", + "moveToBack": "移动到后面", + "moveToFront": "移动到前面", + "addLayer": "添加层", + "addPositivePrompt": "添加 $t(controlLayers.prompt)", + "addNegativePrompt": "添加 $t(controlLayers.negativePrompt)", + "rectangle": "矩形", + "opacity": "透明度", + "canvas": "画布", + "fitBboxToLayers": "将边界框适配到图层", + "cropLayerToBbox": "将图层裁剪到边界框", + "saveBboxToGallery": "将边界框保存到图库", + "savedToGalleryOk": "已保存到图库", + "saveLayerToAssets": "将图层保存到资产", + "removeBookmark": "移除书签", + "regional": "区域", + "saveCanvasToGallery": "将画布保存到图库", + "global": "全局", + "bookmark": "添加书签以快速切换", + "regionalReferenceImage": "局部参考图像", + "mergingLayers": "正在合并图层", + "newControlLayerError": "创建控制层时出现问题", + "pullBboxIntoReferenceImageError": "将边界框导入参考图像时出现问题", + "mergeVisibleOk": "已合并图层", + "maskFill": "遮罩填充", + "newCanvasFromImage": "从图像创建新画布", + "pullBboxIntoReferenceImageOk": "边界框已导入到参考图像", + "addInpaintMask": "添加 $t(controlLayers.inpaintMask)", + "referenceImage": "参考图像", + "globalReferenceImage": "全局参考图像", + "newRegionalGuidance": "新建 $t(controlLayers.regionalGuidance)", + "savedToGalleryError": "保存到图库时出错", + "copyRasterLayerTo": "复制 $t(controlLayers.rasterLayer) 到", + "clearHistory": "清除历史记录", + "inpaintMask": "修复遮罩", + "enableAutoNegative": "启用自动负面提示", + "disableAutoNegative": "禁用自动负面提示", + "deleteReferenceImage": "删除参考图像", + "sendToCanvas": "发送到画布", + "convertRegionalGuidanceTo": "将 $t(controlLayers.regionalGuidance) 转换为", + "newInpaintMask": "新建 $t(controlLayers.inpaintMask)", + "regionIsEmpty": "选定区域为空", + "mergeVisible": "合并可见图层", + "showHUD": "显示 HUD(抬头显示)", + "newLayerFromImage": "从图像创建新图层", + "layer_other": "图层", + "transparency": "透明度", + "addRasterLayer": "添加 $t(controlLayers.rasterLayer)", + "newRasterLayerOk": "已创建栅格层", + "newRasterLayerError": "创建栅格层时出现问题", + "convertRasterLayerTo": "将 $t(controlLayers.rasterLayer) 转换为", + "copyControlLayerTo": "复制 $t(controlLayers.controlLayer) 到", + "copyInpaintMaskTo": "复制 $t(controlLayers.inpaintMask) 到", + "copyRegionalGuidanceTo": "复制 $t(controlLayers.regionalGuidance) 到", + "newRasterLayer": "新建 $t(controlLayers.rasterLayer)", + "newControlLayer": "新建 $t(controlLayers.controlLayer)", + "rasterLayer": "栅格层", + "controlLayer": "控制层", + "outputOnlyMaskedRegions": "仅输出生成的区域", + "addControlLayer": "添加 $t(controlLayers.controlLayer)", + "newGlobalReferenceImageOk": "已创建全局参考图像", + "newGlobalReferenceImageError": "创建全局参考图像时出现问题", + "newRegionalReferenceImageOk": "已创建局部参考图像", + "newControlLayerOk": "已创建控制层", + "mergeVisibleError": "合并图层时出错", + "bboxOverlay": "显示边界框覆盖层", + "clipToBbox": "将Clip限制到边界框", + "width": "宽度", + "inpaintMask_withCount_other": "修复遮罩", + "regionalGuidance_withCount_other": "区域引导", + "newRegionalReferenceImageError": "创建局部参考图像时出现问题", + "pullBboxIntoLayerError": "将边界框导入图层时出现问题", + "pullBboxIntoLayerOk": "边界框已导入到图层", + "rasterLayer_withCount_other": "栅格图层", + "mergeDown": "向下合并", + "clearCaches": "清除缓存", + "recalculateRects": "重新计算矩形", + "duplicate": "复制", + "convertControlLayerTo": "将 $t(controlLayers.controlLayer) 转换为", + "convertInpaintMaskTo": "将 $t(controlLayers.inpaintMask) 转换为", + "copyToClipboard": "复制到剪贴板", + "controlLayer_withCount_other": "控制图层", + "addReferenceImage": "添加 $t(controlLayers.referenceImage)", + "addRegionalGuidance": "添加 $t(controlLayers.regionalGuidance)", + "enableTransparencyEffect": "启用透明效果", + "disableTransparencyEffect": "禁用透明效果", + "hidingType": "隐藏 {{type}}", + "showingType": "显示 {{type}}" + }, + "ui": { + "tabs": { + "queue": "队列", + "canvas": "画布", + "upscaling": "放大中", + "workflows": "工作流", + "models": "模型" + } + }, + "upscaling": { + "structure": "结构", + "upscaleModel": "放大模型", + "missingUpscaleModel": "缺少放大模型", + "missingTileControlNetModel": "没有安装有效的tile ControlNet 模型", + "missingUpscaleInitialImage": "缺少用于放大的原始图像", + "creativity": "创造力", + "postProcessingModel": "后处理模型", + "scale": "缩放", + "tileControlNetModelDesc": "根据所选的主模型架构,选择相应的Tile ControlNet模型", + "upscaleModelDesc": "图像放大(图像到图像转换)模型", + "postProcessingMissingModelWarning": "请访问 模型管理器来安装一个后处理(图像到图像转换)模型.", + "missingModelsWarning": "请访问模型管理器 安装所需的模型:", + "mainModelDesc": "主模型(SD1.5或SDXL架构)", + "exceedsMaxSize": "放大设置超出了最大尺寸限制", + "exceedsMaxSizeDetails": "最大放大限制是 {{maxUpscaleDimension}}x{{maxUpscaleDimension}} 像素.请尝试一个较小的图像或减少您的缩放选择.", + "upscale": "放大" + }, + "stylePresets": { + "positivePrompt": "正向提示词", + "preview": "预览", + "deleteImage": "删除图像", + "deleteTemplate": "删除模版", + "deleteTemplate2": "您确定要删除这个模板吗?请注意,删除后无法恢复.", + "importTemplates": "导入提示模板,支持CSV或JSON格式", + "insertPlaceholder": "插入一个占位符", + "myTemplates": "我的模版", + "name": "名称", + "type": "类型", + "unableToDeleteTemplate": "无法删除提示模板", + "updatePromptTemplate": "更新提示词模版", + "exportPromptTemplates": "导出我的提示模板为CSV格式", + "exportDownloaded": "导出已下载", + "noMatchingTemplates": "无匹配的模版", + "promptTemplatesDesc1": "提示模板可以帮助您在编写提示时添加预设的文本内容.", + "promptTemplatesDesc3": "如果您没有使用占位符,那么模板的内容将会被添加到您提示的末尾.", + "searchByName": "按名称搜索", + "shared": "已分享", + "sharedTemplates": "已分享的模版", + "templateDeleted": "提示模版已删除", + "toggleViewMode": "切换显示模式", + "uploadImage": "上传图像", + "active": "激活", + "choosePromptTemplate": "选择提示词模板", + "clearTemplateSelection": "清除模版选择", + "copyTemplate": "拷贝模版", + "createPromptTemplate": "创建提示词模版", + "defaultTemplates": "默认模版", + "editTemplate": "编辑模版", + "exportFailed": "无法生成并下载CSV文件", + "flatten": "将选定的模板内容合并到当前提示中", + "negativePrompt": "反向提示词", + "promptTemplateCleared": "提示模板已清除", + "useForTemplate": "用于提示词模版", + "viewList": "预览模版列表", + "viewModeTooltip": "这是您的提示在当前选定的模板下的预览效果。如需编辑提示,请直接在文本框中点击进行修改.", + "noTemplates": "无模版", + "private": "私密" + } +} diff --git a/invokeai/frontend/web/public/locales/zh-Hant.json b/invokeai/frontend/web/public/locales/zh-Hant.json new file mode 100644 index 00000000000..ab1f1e6a6d5 --- /dev/null +++ b/invokeai/frontend/web/public/locales/zh-Hant.json @@ -0,0 +1,204 @@ +{ + "common": { + "nodes": "工作流程", + "img2img": "圖片轉圖片", + "statusDisconnected": "已中斷連線", + "back": "返回", + "load": "載入", + "settingsLabel": "設定", + "upload": "上傳", + "discordLabel": "Discord", + "reportBugLabel": "回報錯誤", + "githubLabel": "GitHub", + "hotkeysLabel": "快捷鍵", + "languagePickerLabel": "語言", + "cancel": "取消", + "txt2img": "文字轉圖片", + "controlNet": "ControlNet", + "advanced": "進階", + "folder": "資料夾", + "installed": "已安裝", + "accept": "接受", + "input": "輸入", + "random": "隨機", + "selected": "已選擇", + "communityLabel": "社群", + "loading": "載入中", + "delete": "刪除", + "copy": "複製", + "error": "錯誤", + "file": "檔案", + "format": "格式" + }, + "accessibility": { + "invokeProgressBar": "Invoke 進度條", + "uploadImage": "上傳圖片", + "reset": "重置", + "nextImage": "下一張圖片", + "previousImage": "上一張圖片", + "menu": "選單", + "about": "關於", + "createIssue": "建立問題", + "resetUI": "$t(accessibility.reset) 介面", + "submitSupportTicket": "提交支援工單", + "mode": "模式" + }, + "boards": { + "loading": "載入中…", + "movingImagesToBoard_other": "正在移動 {{count}} 張圖片至板上:", + "move": "移動", + "uncategorized": "未分類", + "cancel": "取消" + }, + "metadata": { + "workflow": "工作流程", + "steps": "步數", + "model": "模型", + "seed": "種子", + "vae": "VAE", + "metadata": "元數據", + "width": "寬度", + "height": "高度" + }, + "accordions": { + "control": { + "title": "控制" + }, + "compositing": { + "title": "合成" + }, + "advanced": { + "title": "進階", + "options": "$t(accordions.advanced.title) 選項" + } + }, + "modelManager": { + "advanced": "進階", + "allModels": "全部模型", + "variant": "變體", + "config": "配置", + "model": "模型", + "selected": "已選擇", + "huggingFace": "HuggingFace", + "install": "安裝", + "metadata": "元數據", + "delete": "刪除", + "description": "描述", + "cancel": "取消", + "convert": "轉換", + "manual": "手動", + "none": "無", + "name": "名稱", + "load": "載入", + "height": "高度", + "width": "寬度", + "search": "搜尋", + "vae": "VAE", + "settings": "設定" + }, + "queue": { + "queue": "佇列", + "canceled": "已取消", + "failed": "已失敗", + "completed": "已完成", + "cancel": "取消", + "session": "工作階段", + "batch": "批量", + "item": "項目", + "completedIn": "完成於", + "notReady": "無法排隊" + }, + "parameters": { + "cancel": { + "cancel": "取消" + }, + "height": "高度", + "type": "類型", + "symmetry": "對稱性", + "images": "圖片", + "width": "寬度", + "coherenceMode": "模式", + "seed": "種子", + "general": "一般", + "strength": "強度", + "steps": "步數", + "info": "資訊" + }, + "settings": { + "beta": "Beta", + "developer": "開發者", + "general": "一般", + "models": "模型" + }, + "popovers": { + "paramModel": { + "heading": "模型" + }, + "compositingCoherenceMode": { + "heading": "模式" + }, + "paramSteps": { + "heading": "步數" + }, + "controlNetProcessor": { + "heading": "處理器" + }, + "paramVAE": { + "heading": "VAE" + }, + "paramHeight": { + "heading": "高度" + }, + "paramSeed": { + "heading": "種子" + }, + "paramWidth": { + "heading": "寬度" + }, + "refinerSteps": { + "heading": "步數" + } + }, + "nodes": { + "workflowName": "名稱", + "notes": "註釋", + "workflowVersion": "版本", + "workflowNotes": "註釋", + "executionStateError": "錯誤", + "unableToUpdateNodes_other": "無法更新 {{count}} 個節點", + "integer": "整數", + "workflow": "工作流程", + "enum": "枚舉", + "edit": "編輯", + "string": "字串", + "workflowTags": "標籤", + "node": "節點", + "boolean": "布林值", + "workflowAuthor": "作者", + "version": "版本", + "executionStateCompleted": "已完成", + "edge": "邊緣" + }, + "sdxl": { + "steps": "步數", + "loading": "載入中…", + "refiner": "精煉器" + }, + "gallery": { + "copy": "複製", + "download": "下載", + "loading": "載入中" + }, + "ui": { + "tabs": { + "models": "模型", + "queue": "佇列" + } + }, + "models": { + "loading": "載入中" + }, + "workflows": { + "name": "名稱" + } +} diff --git a/invokeai/frontend/web/scripts/clean_translations.py b/invokeai/frontend/web/scripts/clean_translations.py new file mode 100644 index 00000000000..a422747ef5c --- /dev/null +++ b/invokeai/frontend/web/scripts/clean_translations.py @@ -0,0 +1,89 @@ +# Cleans translations by removing unused keys +# Usage: python clean_translations.py +# Note: Must be run from invokeai/frontend/web/scripts directory +# +# After running the script, open `en.json` and check for empty objects (`{}`) and remove them manually. +# Also, the script does not handle keys with underscores. They need to be checked manually. + +import json +import os +import re +from typing import TypeAlias, Union + +from tqdm import tqdm + +RecursiveDict: TypeAlias = dict[str, Union["RecursiveDict", str]] + + +class TranslationCleaner: + file_cache: dict[str, str] = {} + + def _get_keys(self, obj: RecursiveDict, current_path: str = "", keys: list[str] | None = None): + if keys is None: + keys = [] + for key in obj: + new_path = f"{current_path}.{key}" if current_path else key + next_ = obj[key] + if isinstance(next_, dict): + self._get_keys(next_, new_path, keys) + elif "_" in key: + # This typically means its a pluralized key + continue + else: + keys.append(new_path) + return keys + + def _search_codebase(self, key: str): + for root, _dirs, files in os.walk("../src"): + for file in files: + if file.endswith(".ts") or file.endswith(".tsx"): + full_path = os.path.join(root, file) + if full_path in self.file_cache: + content = self.file_cache[full_path] + else: + with open(full_path, "r") as f: + content = f.read() + self.file_cache[full_path] = content + + # match the whole key, surrounding by quotes + if re.search(r"['\"`]" + re.escape(key) + r"['\"`]", self.file_cache[full_path]): + return True + # math the stem of the key, with quotes at the end + if re.search(re.escape(key.split(".")[-1]) + r"['\"`]", self.file_cache[full_path]): + return True + return False + + def _remove_key(self, obj: RecursiveDict, key: str): + path = key.split(".") + last_key = path[-1] + for k in path[:-1]: + obj = obj[k] + del obj[last_key] + + def clean(self, obj: RecursiveDict) -> RecursiveDict: + keys = self._get_keys(obj) + pbar = tqdm(keys, desc="Checking keys") + for key in pbar: + if not self._search_codebase(key): + self._remove_key(obj, key) + return obj + + +def main(): + try: + with open("../public/locales/en.json", "r") as f: + data = json.load(f) + except FileNotFoundError as e: + raise FileNotFoundError( + "Unable to find en.json file - must be run from invokeai/frontend/web/scripts directory" + ) from e + + cleaner = TranslationCleaner() + cleaned_data = cleaner.clean(data) + + with open("../public/locales/en.json", "w") as f: + json.dump(cleaned_data, f, indent=4) + + +if __name__ == "__main__": + main() diff --git a/invokeai/frontend/web/scripts/package.json b/invokeai/frontend/web/scripts/package.json new file mode 100644 index 00000000000..985bcf7d652 --- /dev/null +++ b/invokeai/frontend/web/scripts/package.json @@ -0,0 +1,4 @@ +{ + "type": "module", + "packageManager": "pnpm@10.12.4" +} diff --git a/invokeai/frontend/web/scripts/typegen.js b/invokeai/frontend/web/scripts/typegen.js new file mode 100644 index 00000000000..87c00a28833 --- /dev/null +++ b/invokeai/frontend/web/scripts/typegen.js @@ -0,0 +1,104 @@ +/* eslint-disable no-console */ +import fs from 'node:fs'; + +import openapiTS, { astToString } from 'openapi-typescript'; +import ts from 'typescript'; + +const OPENAPI_URL = 'http://127.0.0.1:9090/openapi.json'; +const OUTPUT_FILE = 'src/services/api/schema.ts'; + +async function generateTypes(schema) { + process.stdout.write(`Generating types ${OUTPUT_FILE}...`); + + // Use https://ts-ast-viewer.com to figure out how to create these AST nodes - define a type and use the bottom-left pane's output + // `Blob` type + const BLOB = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Blob')); + // `null` type + const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); + // `Record` type + const RECORD_STRING_UNKNOWN = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Record'), [ + ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), + ]); + + const types = await openapiTS(schema, { + exportType: true, + transform: (schemaObject) => { + if ('format' in schemaObject && schemaObject.format === 'binary') { + return schemaObject.nullable ? ts.factory.createUnionTypeNode([BLOB, NULL]) : BLOB; + } + if (schemaObject.title === 'MetadataField') { + // This is `Record` by default, but it actually accepts any a dict of any valid JSON value. + return RECORD_STRING_UNKNOWN; + } + }, + defaultNonNullable: false, + }); + let output = astToString(types); + + // Post-process: openapi-typescript sometimes computes enum types from `const` + // usage in discriminated unions rather than from the enum definition itself, + // dropping values that only appear in some union members. Patch the generated + // output to match the OpenAPI schema's actual enum definitions. + // + // The `schema` parameter is a parsed JSON object when piped from stdin, or + // a URL/Buffer when passed as an argument. We only patch in the JSON case. + if (schema && typeof schema === 'object' && !Buffer.isBuffer(schema)) { + const schemas = schema.components?.schemas; + if (schemas) { + // Collect all string enum types and their expected values from the OpenAPI schema + for (const [typeName, typeDef] of Object.entries(schemas)) { + if (typeDef && typeDef.type === 'string' && Array.isArray(typeDef.enum)) { + const expectedUnion = typeDef.enum.map((v) => `"${v}"`).join(' | '); + // Match the type definition line. These appear as: + // `TypeName: "val1" | "val2" | ...;` + // Use word boundary to avoid matching types that contain this + // type name as a substring (e.g. ModelType vs BaseModelType). + const regex = new RegExp(`(\\b${typeName}: )"[^;]+(;)`); + const match = output.match(regex); + if (match) { + output = output.replace(regex, `$1${expectedUnion}$2`); + } + } + } + } + } + + fs.writeFileSync(OUTPUT_FILE, output); + process.stdout.write(`\nOK!\r\n`); +} + +function main() { + const encoding = 'utf-8'; + + if (process.stdin.isTTY) { + // Handle generating types with an arg (e.g. URL or path to file) + if (process.argv.length > 3) { + console.error('Usage: typegen.js '); + process.exit(1); + } + if (process.argv[2]) { + const schema = new Buffer.from(process.argv[2], encoding); + generateTypes(schema); + } else { + generateTypes(OPENAPI_URL); + } + } else { + // Handle generating types from stdin + let schema = ''; + process.stdin.setEncoding(encoding); + + process.stdin.on('readable', function () { + const chunk = process.stdin.read(); + if (chunk !== null) { + schema += chunk; + } + }); + + process.stdin.on('end', function () { + generateTypes(JSON.parse(schema)); + }); + } +} + +main(); diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx new file mode 100644 index 00000000000..0f9fb5292b8 --- /dev/null +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -0,0 +1,124 @@ +import { Box, Center, Spinner } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { GlobalHookIsolator } from 'app/components/GlobalHookIsolator'; +import { GlobalModalIsolator } from 'app/components/GlobalModalIsolator'; +import { clearStorage } from 'app/store/enhancers/reduxRemember/driver'; +import Loading from 'common/components/Loading/Loading'; +import { AdministratorSetup } from 'features/auth/components/AdministratorSetup'; +import { LoginPage } from 'features/auth/components/LoginPage'; +import { ProtectedRoute } from 'features/auth/components/ProtectedRoute'; +import { UserManagement } from 'features/auth/components/UserManagement'; +import { UserProfile } from 'features/auth/components/UserProfile'; +import { AppContent } from 'features/ui/components/AppContent'; +import { navigationApi } from 'features/ui/layouts/navigation-api'; +import type { ReactNode } from 'react'; +import { memo, useEffect } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { Route, Routes, useNavigate } from 'react-router-dom'; +import { useGetSetupStatusQuery } from 'services/api/endpoints/auth'; + +import AppErrorBoundaryFallback from './AppErrorBoundaryFallback'; +import ThemeLocaleProvider from './ThemeLocaleProvider'; + +const errorBoundaryOnReset = () => { + clearStorage(); + location.reload(); + return false; +}; + +const MainApp = () => { + const isNavigationAPIConnected = useStore(navigationApi.$isConnected); + return ( + + {isNavigationAPIConnected ? : } + + ); +}; + +const SetupChecker = () => { + const { data, isLoading } = useGetSetupStatusQuery(); + const navigate = useNavigate(); + + // Check if user is already authenticated + const token = localStorage.getItem('auth_token'); + const isAuthenticated = !!token; + + useEffect(() => { + if (!isLoading && data) { + // If multiuser mode is disabled, go directly to the app + if (!data.multiuser_enabled) { + navigate('/app', { replace: true }); + } else if (isAuthenticated) { + // In multiuser mode, check authentication + navigate('/app', { replace: true }); + } else if (data.setup_required) { + navigate('/setup', { replace: true }); + } else { + navigate('/login', { replace: true }); + } + } + }, [data, isLoading, navigate, isAuthenticated]); + + if (isLoading) { + return ( +
+ +
+ ); + } + + return null; +}; + +/** Full-page wrapper for user management / profile pages rendered inside the protected area */ +const FullPageWrapper = ({ children }: { children: ReactNode }) => ( + + {children} + +); + +const App = () => { + return ( + + + + } /> + } /> + } /> + + + + + + } + /> + + + + + + } + /> + + + + } + /> + + + + + + ); +}; + +export default memo(App); diff --git a/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx b/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx new file mode 100644 index 00000000000..f22a94c33fc --- /dev/null +++ b/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx @@ -0,0 +1,76 @@ +import { Button, Flex, Heading, Image, Link, Text } from '@invoke-ai/ui-library'; +import { useClipboard } from 'common/hooks/useClipboard'; +import { toast } from 'features/toast/toast'; +import newGithubIssueUrl from 'new-github-issue-url'; +import InvokeLogoYellow from 'public/assets/images/invoke-symbol-ylw-lrg.svg'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiArrowCounterClockwiseBold, PiArrowSquareOutBold, PiCopyBold } from 'react-icons/pi'; +import { serializeError } from 'serialize-error'; + +type Props = { + error: Error; + resetErrorBoundary: () => void; +}; + +const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => { + const { t } = useTranslation(); + const clipboard = useClipboard(); + + const handleCopy = useCallback(() => { + const text = JSON.stringify(serializeError(error), null, 2); + clipboard.writeText(`\`\`\`\n${text}\n\`\`\``, () => { + toast({ + id: 'ERROR_COPIED', + title: t('toast.errorCopied'), + }); + }); + }, [clipboard, error, t]); + + const url = useMemo(() => { + return newGithubIssueUrl({ + user: 'invoke-ai', + repo: 'InvokeAI', + template: 'BUG_REPORT.yml', + title: `[bug]: ${error.name}: ${error.message}`, + }); + }, [error.message, error.name]); + + return ( + + + + invoke-logo + {t('common.somethingWentWrong')} + + + + + {error.name}: {error.message} + + + + + + + + + + + + ); +}; + +export default memo(AppErrorBoundaryFallback); diff --git a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx new file mode 100644 index 00000000000..c2cdde228d3 --- /dev/null +++ b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx @@ -0,0 +1,76 @@ +import { useGlobalModifiersInit } from '@invoke-ai/ui-library'; +import { setupListeners } from '@reduxjs/toolkit/query'; +import { useSyncFaviconQueueStatus } from 'app/hooks/useSyncFaviconQueueStatus'; +import { useSyncLangDirection } from 'app/hooks/useSyncLangDirection'; +import { useSyncLoggingConfig } from 'app/logging/useSyncLoggingConfig'; +import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/appStarted'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useFocusRegionWatcher } from 'common/hooks/focus'; +import { useCloseChakraTooltipsOnDragFix } from 'common/hooks/useCloseChakraTooltipsOnDragFix'; +import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys'; +import { useTouchDeviceClass } from 'common/hooks/useTouchDeviceClass'; +import { useDndMonitor } from 'features/dnd/useDndMonitor'; +import { useDynamicPromptsWatcher } from 'features/dynamicPrompts/hooks/useDynamicPromptsWatcher'; +import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast'; +import { useWorkflowBuilderWatcher } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher'; +import { useSyncExecutionState } from 'features/nodes/hooks/useNodeExecutionState'; +import { useSyncNodeErrors } from 'features/nodes/store/util/fieldValidators'; +import { useReadinessWatcher } from 'features/queue/store/readiness'; +import { selectLanguage } from 'features/system/store/systemSelectors'; +import { useNavigationApi } from 'features/ui/layouts/use-navigation-api'; +import i18n from 'i18n'; +import { memo, useEffect } from 'react'; +import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo'; +import { useGetQueueCountsByDestinationQuery } from 'services/api/endpoints/queue'; +import { useSocketIO } from 'services/events/useSocketIO'; + +const queueCountArg = { destination: 'canvas' }; + +/** + * GlobalHookIsolator is a logical component that runs global hooks in an isolated component, so that they do not + * cause needless re-renders of any other components. + */ +export const GlobalHookIsolator = memo(() => { + const language = useAppSelector(selectLanguage); + const dispatch = useAppDispatch(); + + // singleton! + useNavigationApi(); + useReadinessWatcher(); + useSocketIO(); + useGlobalModifiersInit(); + useGlobalHotkeys(); + useGetOpenAPISchemaQuery(); + useSyncLoggingConfig(); + useCloseChakraTooltipsOnDragFix(); + useTouchDeviceClass(); + useDndMonitor(); + useSyncNodeErrors(); + useSyncLangDirection(); + + // Persistent subscription to the queue counts query - canvas relies on this to know if there are pending + // and/or in progress canvas sessions. + useGetQueueCountsByDestinationQuery(queueCountArg); + useSyncExecutionState(); + + useEffect(() => { + i18n.changeLanguage(language); + }, [language]); + + useEffect(() => { + dispatch(appStarted()); + }, [dispatch]); + + useEffect(() => { + return setupListeners(dispatch); + }, [dispatch]); + + useStarterModelsToast(); + useSyncFaviconQueueStatus(); + useFocusRegionWatcher(); + useWorkflowBuilderWatcher(); + useDynamicPromptsWatcher(); + + return null; +}); +GlobalHookIsolator.displayName = 'GlobalHookIsolator'; diff --git a/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx b/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx new file mode 100644 index 00000000000..dd1595bdd74 --- /dev/null +++ b/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx @@ -0,0 +1,94 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { useIsRegionFocused } from 'common/hooks/focus'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { useLoadWorkflow } from 'features/gallery/hooks/useLoadWorkflow'; +import { useRecallAll } from 'features/gallery/hooks/useRecallAllImageMetadata'; +import { useRecallDimensions } from 'features/gallery/hooks/useRecallDimensions'; +import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts'; +import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix'; +import { useRecallSeed } from 'features/gallery/hooks/useRecallSeed'; +import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors'; +import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; +import { memo } from 'react'; +import { useImageDTO } from 'services/api/endpoints/images'; +import type { ImageDTO } from 'services/api/types'; + +export const GlobalImageHotkeys = memo(() => { + useAssertSingleton('GlobalImageHotkeys'); + const lastSelectedItem = useAppSelector(selectLastSelectedItem); + const imageDTO = useImageDTO(lastSelectedItem ?? null); + + if (!imageDTO) { + return null; + } + + return ; +}); + +GlobalImageHotkeys.displayName = 'GlobalImageHotkeys'; + +const GlobalImageHotkeysInternal = memo(({ imageDTO }: { imageDTO: ImageDTO }) => { + const isGalleryFocused = useIsRegionFocused('gallery'); + const isViewerFocused = useIsRegionFocused('viewer'); + + const isFocusOK = isGalleryFocused || isViewerFocused; + + const recallAll = useRecallAll(imageDTO); + const recallRemix = useRecallRemix(imageDTO); + const recallPrompts = useRecallPrompts(imageDTO); + const recallSeed = useRecallSeed(imageDTO); + const recallDimensions = useRecallDimensions(imageDTO); + const loadWorkflow = useLoadWorkflow(imageDTO); + + useRegisteredHotkeys({ + id: 'loadWorkflow', + category: 'viewer', + callback: loadWorkflow.load, + options: { enabled: loadWorkflow.isEnabled && isFocusOK }, + dependencies: [loadWorkflow, isFocusOK], + }); + + useRegisteredHotkeys({ + id: 'recallAll', + category: 'viewer', + callback: recallAll.recall, + options: { enabled: recallAll.isEnabled && isFocusOK }, + dependencies: [recallAll, isFocusOK], + }); + + useRegisteredHotkeys({ + id: 'recallSeed', + category: 'viewer', + callback: recallSeed.recall, + options: { enabled: recallSeed.isEnabled && isFocusOK }, + dependencies: [recallSeed, isFocusOK], + }); + + useRegisteredHotkeys({ + id: 'recallPrompts', + category: 'viewer', + callback: recallPrompts.recall, + options: { enabled: recallPrompts.isEnabled && isFocusOK }, + dependencies: [recallPrompts, isFocusOK], + }); + + useRegisteredHotkeys({ + id: 'remix', + category: 'viewer', + callback: recallRemix.recall, + options: { enabled: recallRemix.isEnabled && isFocusOK }, + dependencies: [recallRemix, isFocusOK], + }); + + useRegisteredHotkeys({ + id: 'useSize', + category: 'viewer', + callback: recallDimensions.recall, + options: { enabled: recallDimensions.isEnabled && isFocusOK }, + dependencies: [recallDimensions, isFocusOK], + }); + + return null; +}); + +GlobalImageHotkeysInternal.displayName = 'GlobalImageHotkeysInternal'; diff --git a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx new file mode 100644 index 00000000000..e5ec5ccc565 --- /dev/null +++ b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx @@ -0,0 +1,66 @@ +import { GlobalImageHotkeys } from 'app/components/GlobalImageHotkeys'; +import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal'; +import { CanvasPasteModal } from 'features/controlLayers/components/CanvasPasteModal'; +import { CanvasWorkflowIntegrationModal } from 'features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationModal'; +import { LoadCanvasProjectConfirmationAlertDialog } from 'features/controlLayers/components/LoadCanvasProjectConfirmationAlertDialog'; +import { SaveCanvasProjectDialog } from 'features/controlLayers/components/SaveCanvasProjectDialog'; +import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { CropImageModal } from 'features/cropper/components/CropImageModal'; +import { DeleteImageModal } from 'features/deleteImageModal/components/DeleteImageModal'; +import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone'; +import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal'; +import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal'; +import { ImageContextMenu } from 'features/gallery/components/ContextMenu/ImageContextMenu'; +import { WorkflowLibraryModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal'; +import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog'; +import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog'; +import { DeleteAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog'; +import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog'; +import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal'; +import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal'; +import { VideosModal } from 'features/system/components/VideosModal/VideosModal'; +import { DeleteWorkflowDialog } from 'features/workflowLibrary/components/DeleteLibraryWorkflowConfirmationAlertDialog'; +import { LoadWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog'; +import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal'; +import { NewWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog'; +import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog'; +import { memo } from 'react'; + +/** + * GlobalModalIsolator is a logical component that isolates global modal components, so that they do not cause needless + * re-renders of any other components. + */ +export const GlobalModalIsolator = memo(() => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}); +GlobalModalIsolator.displayName = 'GlobalModalIsolator'; diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx new file mode 100644 index 00000000000..f3d9c4bb28e --- /dev/null +++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx @@ -0,0 +1,65 @@ +import 'i18n'; + +import { configureLogging } from 'app/logging/logger'; +import { addStorageListeners } from 'app/store/enhancers/reduxRemember/driver'; +import { $store } from 'app/store/nanostores/store'; +import { createStore } from 'app/store/store'; +import Loading from 'common/components/Loading/Loading'; +import React, { lazy, memo, useEffect, useState } from 'react'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; + +/* + * We need to configure logging before anything else happens - useLayoutEffect ensures we set this at the first + * possible opportunity. + * + * Once redux initializes, we will check the user's settings and update the logging config accordingly. See + * `useSyncLoggingConfig`. + */ +configureLogging(true, 'debug', '*'); + +const App = lazy(() => import('./App')); + +const InvokeAIUI = () => { + const [store, setStore] = useState | undefined>(undefined); + const [didRehydrate, setDidRehydrate] = useState(false); + + useEffect(() => { + const onRehydrated = () => { + setDidRehydrate(true); + }; + const store = createStore({ persist: true, persistDebounce: 300, onRehydrated }); + setStore(store); + $store.set(store); + if (import.meta.env.MODE === 'development') { + window.$store = $store; + } + const removeStorageListeners = addStorageListeners(); + return () => { + removeStorageListeners(); + setStore(undefined); + $store.set(undefined); + if (import.meta.env.MODE === 'development') { + window.$store = undefined; + } + }; + }, []); + + if (!store || !didRehydrate) { + return ; + } + + return ( + + + + }> + + + + + + ); +}; + +export default memo(InvokeAIUI); diff --git a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx new file mode 100644 index 00000000000..62b7114288d --- /dev/null +++ b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx @@ -0,0 +1,48 @@ +import '@fontsource-variable/inter'; +import 'overlayscrollbars/overlayscrollbars.css'; +import '@xyflow/react/dist/base.css'; +import 'common/components/OverlayScrollbars/overlayscrollbars.css'; +import 'app/components/touchDevice.css'; + +import { ChakraProvider, DarkMode, extendTheme, theme as baseTheme, TOAST_OPTIONS } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { $direction } from 'app/hooks/useSyncLangDirection'; +import type { ReactNode } from 'react'; +import { memo, useMemo } from 'react'; + +type ThemeLocaleProviderProps = { + children: ReactNode; +}; + +const buildTheme = (direction: 'ltr' | 'rtl') => { + return extendTheme({ + ...baseTheme, + direction, + shadows: { + ...baseTheme.shadows, + selected: + 'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-500), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)', + hoverSelected: + 'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-400), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)', + hoverUnselected: + 'inset 0px 0px 0px 2px var(--invoke-colors-invokeBlue-300), inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-800)', + selectedForCompare: + 'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-300), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)', + hoverSelectedForCompare: + 'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-200), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)', + }, + }); +}; + +function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) { + const direction = useStore($direction); + const theme = useMemo(() => buildTheme(direction), [direction]); + + return ( + + {children} + + ); +} + +export default memo(ThemeLocaleProvider); diff --git a/invokeai/frontend/web/src/app/components/touchDevice.css b/invokeai/frontend/web/src/app/components/touchDevice.css new file mode 100644 index 00000000000..7a66951c61a --- /dev/null +++ b/invokeai/frontend/web/src/app/components/touchDevice.css @@ -0,0 +1,5 @@ +/* Hide tooltips after touch input, where hover can get stuck. */ +.invokeai-touch-device [role='tooltip'] { + visibility: hidden !important; + opacity: 0 !important; +} diff --git a/invokeai/frontend/web/src/app/components/touchDevice.test.ts b/invokeai/frontend/web/src/app/components/touchDevice.test.ts new file mode 100644 index 00000000000..69f3835828e --- /dev/null +++ b/invokeai/frontend/web/src/app/components/touchDevice.test.ts @@ -0,0 +1,15 @@ +import { readFileSync } from 'node:fs'; + +import { describe, expect, it } from 'vitest'; + +const css = readFileSync(new URL('./touchDevice.css', import.meta.url), 'utf8'); + +describe('touchDevice.css', () => { + it('hides tooltips only after touch input has been detected', () => { + expect(css).toMatch(/\.invokeai-touch-device\s+\[role='tooltip'\]\s*{/); + }); + + it('does not force all tooltips invisible', () => { + expect(css).not.toMatch(/@media\s*\([^)]*hover[^)]*\)/); + }); +}); diff --git a/invokeai/frontend/web/src/app/constants.ts b/invokeai/frontend/web/src/app/constants.ts new file mode 100644 index 00000000000..b8fab16c1cc --- /dev/null +++ b/invokeai/frontend/web/src/app/constants.ts @@ -0,0 +1,2 @@ +export const NUMPY_RAND_MIN = 0; +export const NUMPY_RAND_MAX = 4294967295; diff --git a/invokeai/frontend/web/src/app/hooks/useSyncFaviconQueueStatus.ts b/invokeai/frontend/web/src/app/hooks/useSyncFaviconQueueStatus.ts new file mode 100644 index 00000000000..7bd55f25f4f --- /dev/null +++ b/invokeai/frontend/web/src/app/hooks/useSyncFaviconQueueStatus.ts @@ -0,0 +1,33 @@ +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { useEffect } from 'react'; +import { useGetQueueStatusQuery } from 'services/api/endpoints/queue'; + +const baseTitle = document.title; +const invokeLogoSVG = 'assets/images/invoke-favicon.svg'; +const invokeAlertLogoSVG = 'assets/images/invoke-alert-favicon.svg'; + +const queryOptions = { + selectFromResult: (res) => ({ + queueSize: res.data ? res.data.queue.pending + res.data.queue.in_progress : 0, + }), +} satisfies Parameters[1]; + +const updateFavicon = (queueSize: number) => { + document.title = queueSize > 0 ? `(${queueSize}) ${baseTitle}` : baseTitle; + const faviconEl = document.getElementById('invoke-favicon'); + if (faviconEl instanceof HTMLLinkElement) { + faviconEl.href = queueSize > 0 ? invokeAlertLogoSVG : invokeLogoSVG; + } +}; + +/** + * This hook synchronizes the queue status with the page's title and favicon. + * It should be considered a singleton and only used once in the component tree. + */ +export const useSyncFaviconQueueStatus = () => { + useAssertSingleton('useSyncFaviconQueueStatus'); + const { queueSize } = useGetQueueStatusQuery(undefined, queryOptions); + useEffect(() => { + updateFavicon(queueSize); + }, [queueSize]); +}; diff --git a/invokeai/frontend/web/src/app/hooks/useSyncLangDirection.ts b/invokeai/frontend/web/src/app/hooks/useSyncLangDirection.ts new file mode 100644 index 00000000000..da1e0dbbcb3 --- /dev/null +++ b/invokeai/frontend/web/src/app/hooks/useSyncLangDirection.ts @@ -0,0 +1,36 @@ +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { atom } from 'nanostores'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; + +/** + * Global atom storing the language direction, to be consumed by the Chakra theme. + * + * Why do we need this? We have a kind of catch-22: + * - The Chakra theme needs to know the language direction to apply the correct styles. + * - The language direction is determined by i18n and the language selection. + * - We want our error boundary to be themed. + * - It's possible that i18n can throw if the language selection is invalid or not supported. + * + * Previously, we had the logic in this file in the theme provider, which wrapped the error boundary. The error + * was properly themed. But then, if i18n threw in the theme provider, the error boundary does not catch the + * error. The app would crash to a white screen. + * + * We tried swapping the component hierarchy so that the error boundary wraps the theme provider, but then the + * error boundary isn't themed! + * + * The solution is to move this i18n direction logic out of the theme provider and into a hook that we can use + * within the error boundary. The error boundary will be themed, _and_ catch any i18n errors. + */ +export const $direction = atom<'ltr' | 'rtl'>('ltr'); + +export const useSyncLangDirection = () => { + useAssertSingleton('useSyncLangDirection'); + const { i18n, t } = useTranslation(); + + useEffect(() => { + const direction = i18n.dir(); + $direction.set(direction); + document.body.dir = direction; + }, [i18n, t]); +}; diff --git a/invokeai/frontend/web/src/app/logging/logger.ts b/invokeai/frontend/web/src/app/logging/logger.ts new file mode 100644 index 00000000000..d20ef77090f --- /dev/null +++ b/invokeai/frontend/web/src/app/logging/logger.ts @@ -0,0 +1,86 @@ +import { createLogWriter } from '@roarr/browser-log-writer'; +import { atom } from 'nanostores'; +import type { Logger, MessageSerializer } from 'roarr'; +import { ROARR, Roarr } from 'roarr'; +import { z } from 'zod'; + +const serializeMessage: MessageSerializer = (message) => { + return JSON.stringify(message); +}; + +ROARR.serializeMessage = serializeMessage; + +const BASE_CONTEXT = {}; + +const $logger = atom(Roarr.child(BASE_CONTEXT)); + +export const zLogNamespace = z.enum([ + 'canvas', + 'canvas-workflow-integration', + 'config', + 'dnd', + 'events', + 'gallery', + 'generation', + 'metadata', + 'models', + 'system', + 'queue', + 'workflows', +]); +export type LogNamespace = z.infer; + +export const logger = (namespace: LogNamespace) => $logger.get().child({ namespace }); + +export const zLogLevel = z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']); +export type LogLevel = z.infer; +export const isLogLevel = (v: unknown): v is LogLevel => zLogLevel.safeParse(v).success; + +// Translate human-readable log levels to numbers, used for log filtering +const LOG_LEVEL_MAP: Record = { + trace: 10, + debug: 20, + info: 30, + warn: 40, + error: 50, + fatal: 60, +}; + +/** + * Configure logging, pushing settings to local storage. + * + * @param logIsEnabled Whether logging is enabled + * @param logLevel The log level + * @param logNamespaces A list of log namespaces to enable, or '*' to enable all + */ +export const configureLogging = ( + logIsEnabled: boolean = true, + logLevel: LogLevel = 'warn', + logNamespaces: LogNamespace[] | '*' +): void => { + if (!logIsEnabled) { + // Disable console log output + localStorage.setItem('ROARR_LOG', 'false'); + } else { + // Enable console log output + localStorage.setItem('ROARR_LOG', 'true'); + + // Use a filter to show only logs of the given level + let filter = `context.logLevel:>=${LOG_LEVEL_MAP[logLevel]}`; + + const namespaces = logNamespaces === '*' ? zLogNamespace.options : logNamespaces; + + if (namespaces.length > 0) { + filter += ` AND (${namespaces.map((ns) => `context.namespace:${ns}`).join(' OR ')})`; + } else { + // This effectively hides all logs because we use namespaces for all logs + filter += ' AND context.namespace:undefined'; + } + + localStorage.setItem('ROARR_FILTER', filter); + } + + const styleOutput = localStorage.getItem('ROARR_STYLE_OUTPUT') === 'false' ? false : true; + + ROARR.write = createLogWriter({ styleOutput }); +}; diff --git a/invokeai/frontend/web/src/app/logging/useSyncLoggingConfig.ts b/invokeai/frontend/web/src/app/logging/useSyncLoggingConfig.ts new file mode 100644 index 00000000000..ca8f26bb3fa --- /dev/null +++ b/invokeai/frontend/web/src/app/logging/useSyncLoggingConfig.ts @@ -0,0 +1,29 @@ +import { configureLogging } from 'app/logging/logger'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { + selectSystemLogIsEnabled, + selectSystemLogLevel, + selectSystemLogNamespaces, +} from 'features/system/store/systemSlice'; +import { useLayoutEffect } from 'react'; + +/** + * This hook synchronizes the logging configuration stored in Redux with the logging system, which uses localstorage. + * + * The sync is one-way: from Redux to localstorage. This means that changes made in the UI will be reflected in the + * logging system, but changes made directly to localstorage will not be reflected in the UI. + * + * See {@link configureLogging} + */ +export const useSyncLoggingConfig = () => { + useAssertSingleton('useSyncLoggingConfig'); + + const logLevel = useAppSelector(selectSystemLogLevel); + const logNamespaces = useAppSelector(selectSystemLogNamespaces); + const logIsEnabled = useAppSelector(selectSystemLogIsEnabled); + + useLayoutEffect(() => { + configureLogging(logIsEnabled, logLevel, logNamespaces); + }, [logIsEnabled, logLevel, logNamespaces]); +}; diff --git a/invokeai/frontend/web/src/app/store/constants.ts b/invokeai/frontend/web/src/app/store/constants.ts new file mode 100644 index 00000000000..381f7f85d26 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/constants.ts @@ -0,0 +1,2 @@ +export const EMPTY_ARRAY = []; +export const EMPTY_OBJECT = {}; diff --git a/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts b/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts new file mode 100644 index 00000000000..7e32afbd3c6 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts @@ -0,0 +1,20 @@ +import { objectEquals } from '@observ33r/object-equals'; +import { createDraftSafeSelectorCreator, createSelectorCreator, lruMemoize } from '@reduxjs/toolkit'; + +/** + * A memoized selector creator that uses LRU cache and @observ33r/object-equals's objectEquals for equality check. + */ +export const createMemoizedSelector = createSelectorCreator({ + memoize: lruMemoize, + memoizeOptions: { + resultEqualityCheck: objectEquals, + }, + argsMemoize: lruMemoize, +}); + +export const getSelectorsOptions = { + createSelector: createDraftSafeSelectorCreator({ + memoize: lruMemoize, + argsMemoize: lruMemoize, + }), +}; diff --git a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts new file mode 100644 index 00000000000..fdb25b37d2c --- /dev/null +++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts @@ -0,0 +1,211 @@ +import { logger } from 'app/logging/logger'; +import { StorageError } from 'app/store/enhancers/reduxRemember/errors'; +import type { UseStore } from 'idb-keyval'; +import { createStore as idbCreateStore, del as idbDel, get as idbGet } from 'idb-keyval'; +import type { Driver } from 'redux-remember'; +import { serializeError } from 'serialize-error'; +import { buildV1Url, getBaseUrl } from 'services/api'; +import type { JsonObject } from 'type-fest'; + +const log = logger('system'); + +const getUrl = (endpoint: 'get_by_key' | 'set_by_key' | 'delete', key?: string) => { + const baseUrl = getBaseUrl(); + const query: Record = {}; + if (key) { + query['key'] = key; + } + + const path = buildV1Url(`client_state/default/${endpoint}`, query); + const url = `${baseUrl}/${path}`; + return url; +}; + +// Persistence happens per slice. To track when persistence is in progress, maintain a ref count, incrementing +// it when a slice is being persisted and decrementing it when the persistence is done. +let persistRefCount = 0; + +// Keep track of the last persisted state for each key to avoid unnecessary network requests. +// +// `redux-remember` persists individual slices of state, so we can implicity denylist a slice by not giving it a +// persist config. +// +// However, we may need to avoid persisting individual _fields_ of a slice. `redux-remember` does not provide a +// way to do this directly. +// +// To accomplish this, we add a layer of logic on top of the `redux-remember`. In the state serializer function +// provided to `redux-remember`, we can omit certain fields from the state that we do not want to persist. See +// the implementation in `store.ts` for this logic. +// +// This logic is unknown to `redux-remember`. When an omitted field changes, it will still attempt to persist the +// whole slice, even if the final, _serialized_ slice value is unchanged. +// +// To avoid unnecessary network requests, we keep track of the last persisted state for each key in this map. +// If the value to be persisted is the same as the last persisted value, we will skip the network request. +const lastPersistedState = new Map(); + +// As of v6.3.0, we use server-backed storage for client state. This replaces the previous IndexedDB-based storage, +// which was implemented using `idb-keyval`. +// +// To facilitate a smooth transition, we implement a migration strategy that attempts to retrieve values from IndexedDB +// and persist them to the new server-backed storage. This is done on a best-effort basis. + +// These constants were used in the previous IndexedDB-based storage implementation. +const IDB_DB_NAME = 'invoke'; +const IDB_STORE_NAME = 'invoke-store'; +const IDB_STORAGE_PREFIX = '@@invokeai-'; + +// Lazy store creation +let _idbKeyValStore: UseStore | null = null; +const getIdbKeyValStore = () => { + if (_idbKeyValStore === null) { + _idbKeyValStore = idbCreateStore(IDB_DB_NAME, IDB_STORE_NAME); + } + return _idbKeyValStore; +}; + +const getIdbKey = (key: string) => { + return `${IDB_STORAGE_PREFIX}${key}`; +}; + +// Helper to get auth headers for client_state requests +const getAuthHeaders = (): Record => { + const headers: Record = {}; + // Safe access to localStorage (not available in Node.js test environment) + if (typeof window !== 'undefined' && window.localStorage) { + const token = localStorage.getItem('auth_token'); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + } + return headers; +}; + +const getItem = async (key: string) => { + try { + const url = getUrl('get_by_key', key); + const res = await fetch(url, { + method: 'GET', + headers: getAuthHeaders(), + }); + if (!res.ok) { + throw new Error(`Response status: ${res.status}`); + } + const value = await res.json(); + + // Best-effort migration from IndexedDB to the new storage system + log.trace({ key, value }, 'Server-backed storage value retrieved'); + + if (!value) { + const idbKey = getIdbKey(key); + try { + // It's a bit tricky to query IndexedDB directly to check if value exists, so we use `idb-keyval` to do it. + // Thing is, `idb-keyval` requires you to create a store to query it. End result - we are creating a store + // even if we don't use it for anything besides checking if the key is present. + const idbKeyValStore = getIdbKeyValStore(); + const idbValue = await idbGet(idbKey, idbKeyValStore); + if (idbValue) { + log.debug( + { key, idbKey, idbValue }, + 'No value in server-backed storage, but found value in IndexedDB - attempting migration' + ); + await idbDel(idbKey, idbKeyValStore); + await setItem(key, idbValue); + log.debug({ key, idbKey, idbValue }, 'Migration successful'); + return idbValue; + } + } catch (error) { + // Just log if IndexedDB retrieval fails - this is a best-effort migration. + log.debug( + { key, idbKey, error: serializeError(error) } as JsonObject, + 'Error checking for or migrating from IndexedDB' + ); + } + } + + lastPersistedState.set(key, value); + log.trace({ key, last: lastPersistedState.get(key), next: value }, `Getting state for ${key}`); + return value; + } catch (originalError) { + throw new StorageError({ + key, + originalError, + }); + } +}; + +const setItem = async (key: string, value: string) => { + try { + persistRefCount++; + if (lastPersistedState.get(key) === value) { + log.trace( + { key, last: lastPersistedState.get(key), next: value }, + `Skipping persist for ${key} as value is unchanged` + ); + return value; + } + log.trace({ key, last: lastPersistedState.get(key), next: value }, `Persisting state for ${key}`); + const url = getUrl('set_by_key', key); + const res = await fetch(url, { + method: 'POST', + body: value, + headers: getAuthHeaders(), + }); + if (!res.ok) { + throw new Error(`Response status: ${res.status}`); + } + const resultValue = await res.json(); + lastPersistedState.set(key, resultValue); + return resultValue; + } catch (originalError) { + throw new StorageError({ + key, + value, + originalError, + }); + } finally { + persistRefCount--; + if (persistRefCount < 0) { + log.trace('Persist ref count is negative, resetting to 0'); + persistRefCount = 0; + } + } +}; + +export const reduxRememberDriver: Driver = { getItem, setItem }; + +export const clearStorage = async () => { + try { + persistRefCount++; + const url = getUrl('delete'); + const res = await fetch(url, { + method: 'POST', + headers: getAuthHeaders(), + }); + if (!res.ok) { + throw new Error(`Response status: ${res.status}`); + } + } catch { + log.error('Failed to reset client state'); + } finally { + persistRefCount--; + lastPersistedState.clear(); + if (persistRefCount < 0) { + log.trace('Persist ref count is negative, resetting to 0'); + persistRefCount = 0; + } + } +}; + +export const addStorageListeners = () => { + const onBeforeUnload = (e: BeforeUnloadEvent) => { + if (persistRefCount > 0) { + e.preventDefault(); + } + }; + window.addEventListener('beforeunload', onBeforeUnload); + + return () => { + window.removeEventListener('beforeunload', onBeforeUnload); + }; +}; diff --git a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/errors.ts b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/errors.ts new file mode 100644 index 00000000000..87c89b27f51 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/errors.ts @@ -0,0 +1,41 @@ +import { logger } from 'app/logging/logger'; +import { PersistError, RehydrateError } from 'redux-remember'; +import { serializeError } from 'serialize-error'; + +type StorageErrorArgs = { + key: string; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ // any is correct + value?: any; + originalError?: unknown; +}; + +export class StorageError extends Error { + key: string; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ // any is correct + value?: any; + originalError?: Error; + + constructor({ key, value, originalError }: StorageErrorArgs) { + super(`Error setting ${key}`); + this.name = 'StorageSetError'; + this.key = key; + if (value !== undefined) { + this.value = value; + } + if (originalError instanceof Error) { + this.originalError = originalError; + } + } +} + +const log = logger('system'); + +export const errorHandler = (err: PersistError | RehydrateError) => { + if (err instanceof PersistError) { + log.error({ error: serializeError(err) }, 'Problem persisting state'); + } else if (err instanceof RehydrateError) { + log.error({ error: serializeError(err) }, 'Problem rehydrating state'); + } else { + log.error({ error: serializeError(err) }, 'Problem in persistence layer'); + } +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/debugLoggerMiddleware.ts b/invokeai/frontend/web/src/app/store/middleware/debugLoggerMiddleware.ts new file mode 100644 index 00000000000..04680b54e10 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/debugLoggerMiddleware.ts @@ -0,0 +1,29 @@ +/* eslint-disable no-console */ +// This is only enabled manually for debugging, console is allowed. + +import type { Middleware, MiddlewareAPI } from '@reduxjs/toolkit'; +import { diff } from 'jsondiffpatch'; + +/** + * Super simple logger middleware. Useful for debugging when the redux devtools are awkward. + */ +export const getDebugLoggerMiddleware = + (options?: { filter?: (action: unknown) => boolean; withDiff?: boolean; withNextState?: boolean }): Middleware => + (api: MiddlewareAPI) => + (next) => + (action) => { + if (options?.filter?.(action)) { + return next(action); + } + const originalState = api.getState(); + console.log('REDUX: dispatching', action); + const result = next(action); + const nextState = api.getState(); + if (options?.withNextState) { + console.log('REDUX: next state', nextState); + } + if (options?.withDiff) { + console.log('REDUX: diff', diff(originalState, nextState)); + } + return result; + }; diff --git a/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts new file mode 100644 index 00000000000..3fba4fba0d0 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts @@ -0,0 +1,13 @@ +import type { UnknownAction } from '@reduxjs/toolkit'; +import { appInfoApi } from 'services/api/endpoints/appInfo'; + +export const actionSanitizer = (action: A): A => { + if (appInfoApi.endpoints.getOpenAPISchema.matchFulfilled(action)) { + return { + ...action, + payload: '', + }; + } + + return action; +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/devtools/actionsDenylist.ts b/invokeai/frontend/web/src/app/store/middleware/devtools/actionsDenylist.ts new file mode 100644 index 00000000000..defb98b64c8 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/actionsDenylist.ts @@ -0,0 +1,16 @@ +/** + * This is a list of actions that should be excluded in the Redux DevTools. + */ +export const actionsDenylist: string[] = [ + // very spammy canvas actions + // 'canvas/setStageCoordinates', + // 'canvas/setStageScale', + // 'canvas/setBoundingBoxCoordinates', + // 'canvas/setBoundingBoxDimensions', + // 'canvas/addPointToCurrentLine', + // bazillions during generation + // 'socket/socketGeneratorProgress', + // 'socket/appSocketGeneratorProgress', + // this happens after every state change + // '@@REMEMBER_PERSISTED', +]; diff --git a/invokeai/frontend/web/src/app/store/middleware/devtools/stateSanitizer.ts b/invokeai/frontend/web/src/app/store/middleware/devtools/stateSanitizer.ts new file mode 100644 index 00000000000..312b4db1897 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/stateSanitizer.ts @@ -0,0 +1,3 @@ +export const stateSanitizer = (state: S): S => { + return state; +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener.ts new file mode 100644 index 00000000000..0ae0f8af6af --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener.ts @@ -0,0 +1,56 @@ +import { createAction } from '@reduxjs/toolkit'; +import { logger } from 'app/logging/logger'; +import type { AppStartListening } from 'app/store/store'; +import { buildAdHocPostProcessingGraph } from 'features/nodes/util/graph/buildAdHocPostProcessingGraph'; +import { toast } from 'features/toast/toast'; +import { t } from 'i18next'; +import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue'; +import type { EnqueueBatchArg, ImageDTO } from 'services/api/types'; +import type { JsonObject } from 'type-fest'; + +const log = logger('queue'); + +export const adHocPostProcessingRequested = createAction<{ imageDTO: ImageDTO }>(`upscaling/postProcessingRequested`); + +export const addAdHocPostProcessingRequestedListener = (startAppListening: AppStartListening) => { + startAppListening({ + actionCreator: adHocPostProcessingRequested, + effect: async (action, { dispatch, getState }) => { + const { imageDTO } = action.payload; + const state = getState(); + + const enqueueBatchArg: EnqueueBatchArg = { + prepend: true, + batch: { + graph: await buildAdHocPostProcessingGraph({ + image: imageDTO, + state, + }), + runs: 1, + }, + }; + + try { + const req = dispatch( + queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, enqueueMutationFixedCacheKeyOptions) + ); + + const enqueueResult = await req.unwrap(); + req.reset(); + log.debug({ enqueueResult } as JsonObject, t('queue.graphQueued')); + } catch (error) { + log.error({ enqueueBatchArg } as JsonObject, t('queue.graphFailedToQueue')); + + if (error instanceof Object && 'status' in error && error.status === 403) { + return; + } else { + toast({ + id: 'GRAPH_QUEUE_FAILED', + title: t('queue.graphFailedToQueue'), + status: 'error', + }); + } + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener.ts new file mode 100644 index 00000000000..b4fc0afd699 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener.ts @@ -0,0 +1,120 @@ +import { isAnyOf } from '@reduxjs/toolkit'; +import type { AppStartListening } from 'app/store/store'; +import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors'; +import { + autoAddBoardIdChanged, + boardIdSelected, + galleryViewChanged, + shouldShowArchivedBoardsChanged, +} from 'features/gallery/store/gallerySlice'; +import { boardsApi } from 'services/api/endpoints/boards'; +import { imagesApi } from 'services/api/endpoints/images'; + +// Type inference doesn't work for this if you inline it in the listener for some reason +const matchAnyBoardDeleted = isAnyOf( + imagesApi.endpoints.deleteBoard.matchFulfilled, + imagesApi.endpoints.deleteBoardAndImages.matchFulfilled +); + +export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartListening) => { + /** + * The auto-add board shouldn't be set to an archived board or deleted board. When we archive a board, delete + * a board, or change a the archived board visibility flag, we may need to reset the auto-add board. + */ + startAppListening({ + matcher: matchAnyBoardDeleted, + effect: (action, { dispatch, getState }) => { + const state = getState(); + const deletedBoardId = action.meta.arg.originalArgs.board_id; + const { autoAddBoardId, selectedBoardId } = state.gallery; + + // If the deleted board was currently selected, we should reset the selected board to uncategorized + if (selectedBoardId !== 'none' && deletedBoardId === selectedBoardId) { + dispatch(boardIdSelected({ boardId: 'none' })); + dispatch(galleryViewChanged('images')); + } + + // If the deleted board was selected for auto-add, we should reset the auto-add board to uncategorized + if (autoAddBoardId !== 'none' && deletedBoardId === autoAddBoardId) { + dispatch(autoAddBoardIdChanged('none')); + } + }, + }); + + // If we archived a board, it may end up hidden. If it's selected or the auto-add board, we should reset those. + startAppListening({ + matcher: boardsApi.endpoints.updateBoard.matchFulfilled, + effect: (action, { dispatch, getState }) => { + const state = getState(); + const { shouldShowArchivedBoards, selectedBoardId, autoAddBoardId } = state.gallery; + + const wasArchived = action.meta.arg.originalArgs.changes.archived === true; + + if (selectedBoardId !== 'none' && autoAddBoardId !== 'none' && wasArchived && !shouldShowArchivedBoards) { + dispatch(autoAddBoardIdChanged('none')); + dispatch(boardIdSelected({ boardId: 'none' })); + dispatch(galleryViewChanged('images')); + } + }, + }); + + // When we hide archived boards, if the selected or the auto-add board is archived, we should reset those. + startAppListening({ + actionCreator: shouldShowArchivedBoardsChanged, + effect: (action, { dispatch, getState }) => { + const shouldShowArchivedBoards = action.payload; + + // We only need to take action if we have just hidden archived boards. + if (shouldShowArchivedBoards) { + return; + } + + const state = getState(); + const queryArgs = selectListBoardsQueryArgs(state); + const queryResult = boardsApi.endpoints.listAllBoards.select(queryArgs)(state); + const { selectedBoardId, autoAddBoardId } = state.gallery; + + if (!queryResult.data) { + return; + } + + // Handle the case where selected board is archived + const selectedBoard = queryResult.data.find((b) => b.board_id === selectedBoardId); + if (selectedBoardId !== 'none' && (!selectedBoard || selectedBoard.archived)) { + // If we can't find the selected board or it's archived, we should reset the selected board to uncategorized + dispatch(boardIdSelected({ boardId: 'none' })); + dispatch(galleryViewChanged('images')); + } + + // Handle the case where auto-add board is archived + const autoAddBoard = queryResult.data.find((b) => b.board_id === autoAddBoardId); + if (autoAddBoardId !== 'none' && (!autoAddBoard || autoAddBoard.archived)) { + // If we can't find the auto-add board or it's archived, we should reset the selected board to uncategorized + dispatch(autoAddBoardIdChanged('none')); + } + }, + }); + + /** + * When listing boards, if the selected or auto-add boards are no longer in the list, we should reset them. + */ + startAppListening({ + matcher: boardsApi.endpoints.listAllBoards.matchFulfilled, + effect: (action, { dispatch, getState }) => { + const boards = action.payload; + const state = getState(); + const { selectedBoardId, autoAddBoardId } = state.gallery; + + // Handle the case where selected board isn't in the list of boards + if (selectedBoardId !== 'none' && !boards.find((b) => b.board_id === selectedBoardId)) { + dispatch(boardIdSelected({ boardId: 'none' })); + dispatch(galleryViewChanged('images')); + } + + // Handle the case where auto-add board isn't in the list of boards + if (autoAddBoardId !== 'none' && !boards.find((b) => b.board_id === autoAddBoardId)) { + dispatch(autoAddBoardIdChanged('none')); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addPBRFilterListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addPBRFilterListener.ts new file mode 100644 index 00000000000..cd0a1d4d528 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addPBRFilterListener.ts @@ -0,0 +1,56 @@ +import { createAction } from '@reduxjs/toolkit'; +import { logger } from 'app/logging/logger'; +import type { AppStartListening } from 'app/store/store'; +import { buildPBRFilterGraph } from 'features/nodes/util/graph/filters/buildPBRFilterGraph'; +import { toast } from 'features/toast/toast'; +import { t } from 'i18next'; +import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue'; +import type { EnqueueBatchArg, ImageDTO } from 'services/api/types'; +import type { JsonObject } from 'type-fest'; + +const log = logger('queue'); + +export const PBRProcessingRequested = createAction<{ imageDTO: ImageDTO }>(`filter/PBRMaps`); + +export const addPBRFilterListener = (startAppListening: AppStartListening) => { + startAppListening({ + actionCreator: PBRProcessingRequested, + effect: async (action, { dispatch, getState }) => { + const { imageDTO } = action.payload; + const state = getState(); + + const enqueueBatchArg: EnqueueBatchArg = { + prepend: true, + batch: { + graph: await buildPBRFilterGraph({ + image: imageDTO, + state, + }), + runs: 1, + }, + }; + + try { + const req = dispatch( + queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, enqueueMutationFixedCacheKeyOptions) + ); + + const enqueueResult = await req.unwrap(); + req.reset(); + log.debug({ enqueueResult } as JsonObject, t('queue.graphQueued')); + } catch (error) { + log.error({ enqueueBatchArg } as JsonObject, t('queue.graphFailedToQueue')); + + if (error instanceof Object && 'status' in error && error.status === 403) { + return; + } else { + toast({ + id: 'GRAPH_QUEUE_FAILED', + title: t('queue.graphFailedToQueue'), + status: 'error', + }); + } + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/anyEnqueued.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/anyEnqueued.ts new file mode 100644 index 00000000000..1d744f8581f --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/anyEnqueued.ts @@ -0,0 +1,21 @@ +import type { AppStartListening } from 'app/store/store'; +import { queueApi, selectQueueStatus } from 'services/api/endpoints/queue'; + +export const addAnyEnqueuedListener = (startAppListening: AppStartListening) => { + startAppListening({ + matcher: queueApi.endpoints.enqueueBatch.matchFulfilled, + effect: (_, { dispatch, getState }) => { + const { data } = selectQueueStatus(getState()); + + if (!data || data.processor.is_started) { + return; + } + + dispatch( + queueApi.endpoints.resumeProcessor.initiate(undefined, { + fixedCacheKey: 'resumeProcessor', + }) + ); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appStarted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appStarted.ts new file mode 100644 index 00000000000..b1d60edc2dc --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appStarted.ts @@ -0,0 +1,51 @@ +import { createAction } from '@reduxjs/toolkit'; +import type { AppStartListening } from 'app/store/store'; +import { noop } from 'es-toolkit'; +import { setInfillMethod } from 'features/controlLayers/store/paramsSlice'; +import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors'; +import { imageSelected } from 'features/gallery/store/gallerySlice'; +import { appInfoApi } from 'services/api/endpoints/appInfo'; +import { imagesApi } from 'services/api/endpoints/images'; + +export const appStarted = createAction('app/appStarted'); + +export const addAppStartedListener = (startAppListening: AppStartListening) => { + startAppListening({ + actionCreator: appStarted, + effect: async (action, { unsubscribe, cancelActiveListeners, take, getState, dispatch }) => { + // this should only run once + cancelActiveListeners(); + unsubscribe(); + + // Fire patchmatch check without blocking the image-selection logic below + dispatch(appInfoApi.endpoints.getPatchmatchStatus.initiate()) + .unwrap() + .then((isPatchmatchAvailable) => { + const infillMethod = getState().params.infillMethod; + + if (!isPatchmatchAvailable && infillMethod === 'patchmatch') { + dispatch(setInfillMethod('lama')); + } + }) + .catch(noop); + + // ensure an image is selected when we load the first board. + // The effect must be async and await take() so that RTK keeps the listener's AbortController + // alive until the query resolves; a synchronous effect causes the controller to be aborted + // immediately after the effect returns, before any network response arrives. + const firstImageLoad = await take(imagesApi.endpoints.getImageNames.matchFulfilled, 5000); + if (firstImageLoad === null) { + // timeout or cancelled + return; + } + const [{ payload }] = firstImageLoad; + const selectedImage = selectLastSelectedItem(getState()); + if (selectedImage) { + return; + } + if (payload.image_names[0]) { + dispatch(imageSelected(payload.image_names[0])); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts new file mode 100644 index 00000000000..fae7436d2e1 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts @@ -0,0 +1,74 @@ +import { logger } from 'app/logging/logger'; +import type { AppStartListening } from 'app/store/store'; +import { truncate } from 'es-toolkit/compat'; +import { zPydanticValidationError } from 'features/system/store/zodSchemas'; +import { toast } from 'features/toast/toast'; +import { t } from 'i18next'; +import { serializeError } from 'serialize-error'; +import { queueApi } from 'services/api/endpoints/queue'; +import type { JsonObject } from 'type-fest'; + +const log = logger('queue'); + +export const addBatchEnqueuedListener = (startAppListening: AppStartListening) => { + // success + startAppListening({ + matcher: queueApi.endpoints.enqueueBatch.matchFulfilled, + effect: (action) => { + const enqueueResult = action.payload; + const arg = action.meta.arg.originalArgs; + log.debug({ enqueueResult } as JsonObject, 'Batch enqueued'); + + toast({ + id: 'QUEUE_BATCH_SUCCEEDED', + title: t('queue.batchQueued'), + status: 'success', + description: t('queue.batchQueuedDesc', { + count: enqueueResult.enqueued, + direction: arg.prepend ? t('queue.front') : t('queue.back'), + }), + }); + }, + }); + + // error + startAppListening({ + matcher: queueApi.endpoints.enqueueBatch.matchRejected, + effect: (action) => { + const response = action.payload; + const batchConfig = action.meta.arg.originalArgs; + + if (!response) { + toast({ + id: 'QUEUE_BATCH_FAILED', + title: t('queue.batchFailedToQueue'), + status: 'error', + description: t('common.unknownError'), + }); + log.error({ batchConfig } as JsonObject, t('queue.batchFailedToQueue')); + return; + } + + const result = zPydanticValidationError.safeParse(response); + if (result.success) { + result.data.data.detail.map((e) => { + const description = truncate(e.msg.replace(/^(Value|Index|Key) error, /i, ''), { length: 256 }); + toast({ + id: 'QUEUE_BATCH_FAILED', + title: t('queue.batchFailedToQueue'), + status: 'error', + description, + }); + }); + } else if (response.status !== 403) { + toast({ + id: 'QUEUE_BATCH_FAILED', + title: t('queue.batchFailedToQueue'), + status: 'error', + description: t('common.unknownError'), + }); + } + log.error({ batchConfig, error: serializeError(response) } as JsonObject, t('queue.batchFailedToQueue')); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts new file mode 100644 index 00000000000..d185a03f220 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts @@ -0,0 +1,36 @@ +import type { AppStartListening } from 'app/store/store'; +import { selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { getImageUsage } from 'features/deleteImageModal/store/state'; +import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; +import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice'; +import { imagesApi } from 'services/api/endpoints/images'; + +export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppStartListening) => { + startAppListening({ + matcher: imagesApi.endpoints.deleteBoardAndImages.matchFulfilled, + effect: (action, { dispatch, getState }) => { + const { deleted_images } = action.payload; + + // Remove all deleted images from the UI + + let wasNodeEditorReset = false; + + const state = getState(); + const nodes = selectNodesSlice(state); + const canvas = selectCanvasSlice(state); + const upscale = selectUpscaleSlice(state); + const refImages = selectRefImagesSlice(state); + + deleted_images.forEach((image_name) => { + const imageUsage = getImageUsage(nodes, canvas, upscale, refImages, image_name); + + if (imageUsage.isNodesImage && !wasNodeEditorReset) { + dispatch(nodeEditorReset()); + wasNodeEditorReset = true; + } + }); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts new file mode 100644 index 00000000000..9fd777fb29b --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts @@ -0,0 +1,44 @@ +import { isAnyOf } from '@reduxjs/toolkit'; +import type { AppStartListening } from 'app/store/store'; +import { selectGetImageNamesQueryArgs, selectSelectedBoardId } from 'features/gallery/store/gallerySelectors'; +import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice'; +import { imagesApi } from 'services/api/endpoints/images'; + +export const addBoardIdSelectedListener = (startAppListening: AppStartListening) => { + startAppListening({ + matcher: isAnyOf(boardIdSelected, galleryViewChanged), + effect: async (action, { getState, dispatch, condition, cancelActiveListeners }) => { + // Cancel any in-progress instances of this listener, we don't want to select an image from a previous board + cancelActiveListeners(); + + if (boardIdSelected.match(action) && action.payload.select) { + // This action already has a resource selection - skip the below auto-selection logic + return; + } + + const state = getState(); + + const board_id = selectSelectedBoardId(state); + + const queryArgs = { ...selectGetImageNamesQueryArgs(state), board_id }; + // wait until the board has some images - maybe it already has some from a previous fetch + // must use getState() to ensure we do not have stale state + const isSuccess = await condition( + () => imagesApi.endpoints.getImageNames.select(queryArgs)(getState()).isSuccess, + 5000 + ); + + if (!isSuccess) { + dispatch(imageSelected(null)); + return; + } + + // the board was just changed - we can select the first image + const imageNames = imagesApi.endpoints.getImageNames.select(queryArgs)(getState()).data?.image_names; + + const imageToSelect = imageNames && imageNames.length > 0 ? imageNames[0] : null; + + dispatch(imageSelected(imageToSelect ?? null)); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx new file mode 100644 index 00000000000..fa4c29b8f42 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx @@ -0,0 +1,45 @@ +import { logger } from 'app/logging/logger'; +import type { AppStartListening } from 'app/store/store'; +import { toast } from 'features/toast/toast'; +import { t } from 'i18next'; +import { imagesApi } from 'services/api/endpoints/images'; + +const log = logger('gallery'); + +export const addBulkDownloadListeners = (startAppListening: AppStartListening) => { + startAppListening({ + matcher: imagesApi.endpoints.bulkDownloadImages.matchFulfilled, + effect: (action) => { + log.debug(action.payload, 'Bulk download requested'); + + // Use a "preparing:" prefix so this toast cannot collide with the + // "ready to download" toast that arrives via the bulk_download_complete + // socket event. The background task can complete in under 20ms, so the + // socket event may arrive *before* this Redux middleware runs — without + // distinct IDs the "preparing" toast would overwrite the "ready" toast. + const itemName = action.payload.bulk_download_item_name; + toast({ + id: itemName ? `preparing:${itemName}` : undefined, + title: t('gallery.bulkDownloadRequested'), + status: 'success', + // Show the response message if it exists, otherwise show the default message + description: action.payload.response || t('gallery.bulkDownloadRequestedDesc'), + duration: null, + }); + }, + }); + + startAppListening({ + matcher: imagesApi.endpoints.bulkDownloadImages.matchRejected, + effect: () => { + log.debug('Bulk download request failed'); + + // There isn't any toast to update if we get this event. + toast({ + id: 'BULK_DOWNLOAD_REQUEST_FAILED', + title: t('gallery.bulkDownloadRequestFailed'), + status: 'error', + }); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts new file mode 100644 index 00000000000..416c77b9dd7 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts @@ -0,0 +1,39 @@ +import { logger } from 'app/logging/logger'; +import type { AppStartListening } from 'app/store/store'; +import { parseify } from 'common/util/serialize'; +import { size } from 'es-toolkit/compat'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import { parseSchema } from 'features/nodes/util/schema/parseSchema'; +import { serializeError } from 'serialize-error'; +import { appInfoApi } from 'services/api/endpoints/appInfo'; +import type { JsonObject } from 'type-fest'; + +const log = logger('system'); + +export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening) => { + startAppListening({ + matcher: appInfoApi.endpoints.getOpenAPISchema.matchFulfilled, + effect: (action) => { + const schemaJSON = action.payload; + + log.debug({ schemaJSON: parseify(schemaJSON) } as JsonObject, 'Received OpenAPI schema'); + + const nodeTemplates = parseSchema(schemaJSON); + + log.debug({ nodeTemplates } as JsonObject, `Built ${size(nodeTemplates)} node templates`); + + $templates.set(nodeTemplates); + }, + }); + + startAppListening({ + matcher: appInfoApi.endpoints.getOpenAPISchema.matchRejected, + effect: (action) => { + // If action.meta.condition === true, the request was canceled/skipped because another request was in flight or + // the value was already in the cache. We don't want to log these errors. + if (!action.meta.condition) { + log.error({ error: serializeError(action.error) }, 'Problem retrieving OpenAPI Schema'); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts new file mode 100644 index 00000000000..beb963a198f --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts @@ -0,0 +1,23 @@ +import { logger } from 'app/logging/logger'; +import type { AppStartListening } from 'app/store/store'; +import { imagesApi } from 'services/api/endpoints/images'; + +const log = logger('gallery'); + +export const addImageAddedToBoardFulfilledListener = (startAppListening: AppStartListening) => { + startAppListening({ + matcher: imagesApi.endpoints.addImageToBoard.matchFulfilled, + effect: (action) => { + const { board_id, image_name } = action.meta.arg.originalArgs; + log.debug({ board_id, image_name }, 'Image added to board'); + }, + }); + + startAppListening({ + matcher: imagesApi.endpoints.addImageToBoard.matchRejected, + effect: (action) => { + const { board_id, image_name } = action.meta.arg.originalArgs; + log.debug({ board_id, image_name }, 'Problem adding image to board'); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts new file mode 100644 index 00000000000..2ee25dea329 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts @@ -0,0 +1,23 @@ +import { logger } from 'app/logging/logger'; +import type { AppStartListening } from 'app/store/store'; +import { imagesApi } from 'services/api/endpoints/images'; + +const log = logger('gallery'); + +export const addImageRemovedFromBoardFulfilledListener = (startAppListening: AppStartListening) => { + startAppListening({ + matcher: imagesApi.endpoints.removeImageFromBoard.matchFulfilled, + effect: (action) => { + const imageDTO = action.meta.arg.originalArgs; + log.debug({ imageDTO }, 'Image removed from board'); + }, + }); + + startAppListening({ + matcher: imagesApi.endpoints.removeImageFromBoard.matchRejected, + effect: (action) => { + const imageDTO = action.meta.arg.originalArgs; + log.debug({ imageDTO }, 'Problem removing image from board'); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts new file mode 100644 index 00000000000..f421009030f --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -0,0 +1,112 @@ +import { logger } from 'app/logging/logger'; +import type { AppStartListening, RootState } from 'app/store/store'; +import { omit } from 'es-toolkit/compat'; +import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors'; +import { boardIdSelected, galleryViewChanged } from 'features/gallery/store/gallerySlice'; +import { toast } from 'features/toast/toast'; +import { t } from 'i18next'; +import { boardsApi } from 'services/api/endpoints/boards'; +import { imagesApi } from 'services/api/endpoints/images'; +import type { ImageDTO } from 'services/api/types'; +const log = logger('gallery'); + +/** + * Gets the description for the toast that is shown when an image is uploaded. + * @param boardId The board id of the uploaded image + * @param state The current state of the app + * @returns + */ +const getUploadedToastDescription = (boardId: string, state: RootState) => { + if (boardId === 'none') { + return t('toast.addedToUncategorized'); + } + // Attempt to get the board's name for the toast + const queryArgs = selectListBoardsQueryArgs(state); + const { data } = boardsApi.endpoints.listAllBoards.select(queryArgs)(state); + // Fall back to just the board id if we can't find the board for some reason + const board = data?.find((b) => b.board_id === boardId); + + return t('toast.addedToBoard', { name: board?.board_name ?? boardId }); +}; + +let lastUploadedToastTimeout: number | null = null; + +export const addImageUploadedFulfilledListener = (startAppListening: AppStartListening) => { + startAppListening({ + matcher: imagesApi.endpoints.uploadImage.matchFulfilled, + effect: (action, { dispatch, getState }) => { + let imageDTO: ImageDTO; + let silent; + let isFirstUploadOfBatch = true; + imageDTO = action.payload; + silent = action.meta.arg.originalArgs.silent; + isFirstUploadOfBatch = action.meta.arg.originalArgs.isFirstUploadOfBatch ?? true; + + if (silent || imageDTO.is_intermediate) { + // If the image is silent or intermediate, we don't want to show a toast + return; + } + + const state = getState(); + + log.debug({ imageDTO }, 'Image uploaded'); + + const boardId = imageDTO.board_id ?? 'none'; + + const DEFAULT_UPLOADED_TOAST = { + id: 'IMAGE_UPLOADED', + title: t('toast.imageUploaded'), + status: 'success', + } as const; + + // default action - just upload and alert user + if (lastUploadedToastTimeout !== null) { + window.clearTimeout(lastUploadedToastTimeout); + } + const toastApi = toast({ + ...DEFAULT_UPLOADED_TOAST, + title: DEFAULT_UPLOADED_TOAST.title, + description: getUploadedToastDescription(boardId, state), + duration: null, // we will close the toast manually + }); + lastUploadedToastTimeout = window.setTimeout(() => { + toastApi.close(); + }, 3000); + + /** + * We only want to change the board and view if this is the first upload of a batch, else we end up hijacking + * the user's gallery board and view selection: + * - User uploads multiple images + * - A couple uploads finish, but others are pending still + * - User changes the board selection + * - Pending uploads finish and change the board back to the original board + * - User is confused as to why the board changed + * + * Default to true to not require _all_ image upload handlers to set this value + */ + + if (isFirstUploadOfBatch) { + dispatch(boardIdSelected({ boardId })); + dispatch(galleryViewChanged('assets')); + } + }, + }); + + startAppListening({ + matcher: imagesApi.endpoints.uploadImage.matchRejected, + effect: (action) => { + const sanitizedData = { + arg: { + ...omit(action.meta.arg.originalArgs, ['file', 'postUploadAction']), + file: '', + }, + }; + log.error({ ...sanitizedData }, 'Image upload failed'); + toast({ + title: t('toast.imageUploadFailed'), + description: action.error.message, + status: 'error', + }); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.test.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.test.ts new file mode 100644 index 00000000000..2dab056cf69 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.test.ts @@ -0,0 +1,312 @@ +import { zModelIdentifierField } from 'features/nodes/types/common'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +// Mock model configs returned by selectors - these simulate what RTK Query provides +const mockAnimaQwen3Encoder = { + key: 'qwen3-06b-key', + hash: 'qwen3-06b-hash', + name: 'Qwen3 0.6B Encoder', + base: 'any' as const, + type: 'qwen3_encoder' as const, + variant: 'qwen3_06b' as const, + format: 'qwen3_encoder' as const, +}; + +const mockAnimaVAE = { + key: 'anima-vae-key', + hash: 'anima-vae-hash', + name: 'Anima VAE', + base: 'anima' as const, + type: 'vae' as const, + format: 'diffusers' as const, +}; + +const mockAnimaMainModel = { + key: 'anima-main-key', + hash: 'anima-main-hash', + name: 'Anima Generate', + base: 'anima' as const, + type: 'main' as const, +}; + +const mockFluxMainModel = { + key: 'flux-main-key', + hash: 'flux-main-hash', + name: 'FLUX.1 Dev', + base: 'flux' as const, + type: 'main' as const, +}; + +// Track dispatched actions +const dispatched: Array<{ type: string; payload: unknown }> = []; +const mockDispatch = vi.fn((action: { type: string; payload: unknown }) => { + dispatched.push(action); +}); + +// Mock logger +vi.mock('app/logging/logger', () => ({ + logger: () => ({ + debug: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + }), +})); + +// Mock toast +vi.mock('features/toast/toast', () => ({ + toast: vi.fn(), +})); + +// Mock i18next +vi.mock('i18next', () => ({ + t: (key: string) => key, +})); + +// Mock model selectors from RTK Query hooks + +const mockSelectAnimaQwen3EncoderModels = vi.fn((_state: unknown) => [mockAnimaQwen3Encoder]); + +const mockSelectAnimaVAEModels = vi.fn((_state: unknown) => [mockAnimaVAE]); + +vi.mock('services/api/hooks/modelsByType', () => ({ + selectAnimaQwen3EncoderModels: (state: unknown) => mockSelectAnimaQwen3EncoderModels(state), + selectAnimaVAEModels: (state: unknown) => mockSelectAnimaVAEModels(state), + selectQwen3EncoderModels: vi.fn(() => []), + selectZImageDiffusersModels: vi.fn(() => []), + selectFluxVAEModels: vi.fn(() => []), + selectGlobalRefImageModels: vi.fn(() => []), + selectRegionalRefImageModels: vi.fn(() => []), +})); + +// Mock model configs adapter +vi.mock('services/api/endpoints/models', () => ({ + modelConfigsAdapterSelectors: { selectById: vi.fn() }, + selectModelConfigsQuery: vi.fn(() => ({ data: undefined })), +})); + +vi.mock('services/api/types', () => ({ + isFluxKontextModelConfig: vi.fn(() => false), + isFluxReduxModelConfig: vi.fn(() => false), +})); + +// Mock canvas selectors +vi.mock('features/controlLayers/store/canvasStagingAreaSlice', () => ({ + buildSelectIsStaging: vi.fn(() => vi.fn(() => false)), + selectCanvasSessionId: vi.fn(() => null), +})); + +vi.mock('features/controlLayers/store/selectors', () => ({ + selectAllEntitiesOfType: vi.fn(() => []), + selectBboxModelBase: vi.fn(() => 'anima'), + selectCanvasSlice: vi.fn(() => ({})), +})); + +vi.mock('features/controlLayers/store/refImagesSlice', () => ({ + refImageConfigChanged: vi.fn(), + refImageModelChanged: vi.fn(), + selectReferenceImageEntities: vi.fn(() => []), +})); + +vi.mock('features/controlLayers/store/types', async () => { + const actual = await vi.importActual('features/controlLayers/store/types'); + return { + ...(actual as Record), + getEntityIdentifier: vi.fn(), + isFlux2ReferenceImageConfig: vi.fn(() => false), + }; +}); + +vi.mock('features/controlLayers/store/util', () => ({ + initialFlux2ReferenceImage: {}, + initialFluxKontextReferenceImage: {}, + initialFLUXRedux: {}, + initialIPAdapter: {}, +})); + +vi.mock('features/modelManagerV2/models', () => ({ + SUPPORTS_REF_IMAGES_BASE_MODELS: ['sd-1', 'sdxl', 'flux', 'flux2'], +})); + +vi.mock('features/controlLayers/store/canvasSlice', () => ({ + bboxSyncedToOptimalDimension: vi.fn(() => ({ type: 'bboxSyncedToOptimalDimension' })), + rgRefImageModelChanged: vi.fn(), +})); + +vi.mock('features/controlLayers/store/lorasSlice', () => ({ + loraIsEnabledChanged: vi.fn((payload: unknown) => ({ type: 'loraIsEnabledChanged', payload })), +})); + +// Capture the listener effect so we can call it directly +let capturedEffect: ((action: unknown, api: unknown) => void) | null = null; + +// Import actual action creators for assertion matching +const paramsSliceActual = (await vi.importActual('features/controlLayers/store/paramsSlice')) as { + animaQwen3EncoderModelSelected: { type: string }; + animaVaeModelSelected: { type: string }; +}; +const { animaQwen3EncoderModelSelected, animaVaeModelSelected } = paramsSliceActual; + +// Import after mocks are set up +const { addModelSelectedListener } = await import('./modelSelected'); +const { modelSelected } = await import('features/parameters/store/actions'); +const { zParameterModel } = await import('features/parameters/types/parameterSchemas'); + +// Capture the effect +addModelSelectedListener(((config: { effect: typeof capturedEffect }) => { + capturedEffect = config.effect; +}) as never); + +function buildMockState(overrides: Record = {}) { + return { + params: { + model: null, + vae: null, + zImageVaeModel: null, + zImageQwen3EncoderModel: null, + zImageQwen3SourceModel: null, + animaVaeModel: null, + animaQwen3EncoderModel: null, + animaScheduler: 'euler', + kleinVaeModel: null, + kleinQwen3EncoderModel: null, + zImageScheduler: 'euler', + ...overrides, + }, + loras: { loras: [] }, + canvas: {}, + }; +} + +describe('modelSelected listener - Anima defaulting', () => { + beforeEach(() => { + dispatched.length = 0; + mockDispatch.mockClear(); + mockSelectAnimaQwen3EncoderModels.mockReturnValue([mockAnimaQwen3Encoder]); + mockSelectAnimaVAEModels.mockReturnValue([mockAnimaVAE]); + }); + + it('should dispatch encoder models with full ModelIdentifierField payloads when switching to Anima', () => { + const state = buildMockState({ model: mockFluxMainModel }); + const action = modelSelected(zParameterModel.parse(mockAnimaMainModel)); + + capturedEffect!(action, { + getState: () => state, + dispatch: mockDispatch, + }); + + // Find the dispatched actions for Anima encoders + const qwen3Dispatch = dispatched.find((a) => a.type === animaQwen3EncoderModelSelected.type); + const vaeDispatch = dispatched.find((a) => a.type === animaVaeModelSelected.type); + + // Both should have been dispatched + expect(qwen3Dispatch).toBeDefined(); + expect(vaeDispatch).toBeDefined(); + + // The payloads must pass zModelIdentifierField validation (the actual schema used by reducers) + expect(zModelIdentifierField.safeParse(qwen3Dispatch!.payload).success).toBe(true); + expect(zModelIdentifierField.safeParse(vaeDispatch!.payload).success).toBe(true); + }); + + it('should include hash and type in Qwen3 encoder payload', () => { + const state = buildMockState({ model: mockFluxMainModel }); + const action = modelSelected(zParameterModel.parse(mockAnimaMainModel)); + + capturedEffect!(action, { + getState: () => state, + dispatch: mockDispatch, + }); + + const qwen3Dispatch = dispatched.find((a) => a.type === animaQwen3EncoderModelSelected.type); + expect(qwen3Dispatch!.payload).toMatchObject({ + key: mockAnimaQwen3Encoder.key, + hash: mockAnimaQwen3Encoder.hash, + name: mockAnimaQwen3Encoder.name, + base: mockAnimaQwen3Encoder.base, + type: mockAnimaQwen3Encoder.type, + }); + }); + + it('should not dispatch encoder defaults when Anima models are already set', () => { + const existingQwen3 = { key: 'existing', hash: 'h', name: 'Existing', base: 'any', type: 'qwen3_encoder' }; + const existingVae = { key: 'existing-vae', hash: 'h', name: 'Existing VAE', base: 'anima', type: 'vae' }; + + const state = buildMockState({ + model: mockFluxMainModel, + animaQwen3EncoderModel: existingQwen3, + animaVaeModel: existingVae, + }); + + const action = modelSelected(zParameterModel.parse(mockAnimaMainModel)); + + capturedEffect!(action, { + getState: () => state, + dispatch: mockDispatch, + }); + + // Should NOT dispatch any encoder model selections since they're already set + const qwen3Dispatch = dispatched.find((a) => a.type === animaQwen3EncoderModelSelected.type); + const vaeDispatch = dispatched.find((a) => a.type === animaVaeModelSelected.type); + + expect(qwen3Dispatch).toBeUndefined(); + expect(vaeDispatch).toBeUndefined(); + }); + + it('should not dispatch encoder defaults when no encoder models are available', () => { + mockSelectAnimaQwen3EncoderModels.mockReturnValue([]); + mockSelectAnimaVAEModels.mockReturnValue([]); + + const state = buildMockState({ model: mockFluxMainModel }); + const action = modelSelected(zParameterModel.parse(mockAnimaMainModel)); + + capturedEffect!(action, { + getState: () => state, + dispatch: mockDispatch, + }); + + const qwen3Dispatch = dispatched.find((a) => a.type === animaQwen3EncoderModelSelected.type); + const vaeDispatch = dispatched.find((a) => a.type === animaVaeModelSelected.type); + + expect(qwen3Dispatch).toBeUndefined(); + expect(vaeDispatch).toBeUndefined(); + }); + + it('should clear Anima models when switching away from Anima', () => { + const existingQwen3 = { key: 'existing', hash: 'h', name: 'Existing', base: 'any', type: 'qwen3_encoder' }; + const existingVae = { key: 'existing-vae', hash: 'h', name: 'Existing VAE', base: 'anima', type: 'vae' }; + + const state = buildMockState({ + model: mockAnimaMainModel, + animaQwen3EncoderModel: existingQwen3, + animaVaeModel: existingVae, + }); + + const action = modelSelected(zParameterModel.parse(mockFluxMainModel)); + + capturedEffect!(action, { + getState: () => state, + dispatch: mockDispatch, + }); + + // Should dispatch null for both + const qwen3Dispatch = dispatched.find((a) => a.type === animaQwen3EncoderModelSelected.type); + const vaeDispatch = dispatched.find((a) => a.type === animaVaeModelSelected.type); + + expect(qwen3Dispatch).toBeDefined(); + expect(qwen3Dispatch!.payload).toBeNull(); + expect(vaeDispatch).toBeDefined(); + expect(vaeDispatch!.payload).toBeNull(); + }); +}); + +describe('zModelIdentifierField schema validation', () => { + it('should reject payloads missing hash and type', () => { + const incomplete = { key: 'some-key', name: 'Some Model', base: 'any' }; + expect(zModelIdentifierField.safeParse(incomplete).success).toBe(false); + }); + + it('should accept payloads with all required fields', () => { + const complete = { key: 'some-key', hash: 'some-hash', name: 'Some Model', base: 'any', type: 'qwen3_encoder' }; + expect(zModelIdentifierField.safeParse(complete).success).toBe(true); + }); +}); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts new file mode 100644 index 00000000000..9e67e013946 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -0,0 +1,583 @@ +import { logger } from 'app/logging/logger'; +import type { AppStartListening } from 'app/store/store'; +import { bboxSyncedToOptimalDimension, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice'; +import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { loraIsEnabledChanged } from 'features/controlLayers/store/lorasSlice'; +import { + animaQwen3EncoderModelSelected, + animaVaeModelSelected, + aspectRatioIdChanged, + kleinQwen3EncoderModelSelected, + kleinVaeModelSelected, + modelChanged, + qwenImageComponentSourceSelected, + qwenImageQwenVLEncoderModelSelected, + qwenImageVaeModelSelected, + resolutionPresetSelected, + setZImageScheduler, + syncedToOptimalDimension, + vaeSelected, + zImageQwen3EncoderModelSelected, + zImageQwen3SourceModelSelected, + zImageVaeModelSelected, +} from 'features/controlLayers/store/paramsSlice'; +import { + refImageConfigChanged, + refImageModelChanged, + selectReferenceImageEntities, +} from 'features/controlLayers/store/refImagesSlice'; +import { + selectAllEntitiesOfType, + selectBboxModelBase, + selectCanvasSlice, +} from 'features/controlLayers/store/selectors'; +import { + getEntityIdentifier, + isAspectRatioID, + isFlux2ReferenceImageConfig, + isQwenImageReferenceImageConfig, +} from 'features/controlLayers/store/types'; +import { + initialFlux2ReferenceImage, + initialFluxKontextReferenceImage, + initialFLUXRedux, + initialIPAdapter, + initialQwenImageReferenceImage, +} from 'features/controlLayers/store/util'; +import { SUPPORTS_REF_IMAGES_BASE_MODELS } from 'features/modelManagerV2/models'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import { modelSelected } from 'features/parameters/store/actions'; +import { zParameterModel } from 'features/parameters/types/parameterSchemas'; +import { toast } from 'features/toast/toast'; +import { t } from 'i18next'; +import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models'; +import { + selectAnimaQwen3EncoderModels, + selectAnimaVAEModels, + selectFluxVAEModels, + selectGlobalRefImageModels, + selectQwen3EncoderModels, + selectQwenImageDiffusersModels, + selectQwenImageVAEModels, + selectQwenVLEncoderModels, + selectRegionalRefImageModels, + selectZImageDiffusersModels, +} from 'services/api/hooks/modelsByType'; +import type { FLUXKontextModelConfig, FLUXReduxModelConfig, IPAdapterModelConfig } from 'services/api/types'; +import { isExternalApiModelConfig, isFluxKontextModelConfig, isFluxReduxModelConfig } from 'services/api/types'; + +const log = logger('models'); + +export const addModelSelectedListener = (startAppListening: AppStartListening) => { + startAppListening({ + actionCreator: modelSelected, + effect: (action, { getState, dispatch }) => { + const state = getState(); + const result = zParameterModel.safeParse(action.payload); + + if (!result.success) { + log.error({ error: result.error.format() }, 'Failed to parse main model'); + return; + } + + const newModel = result.data; + const newBase = newModel.base; + const didBaseModelChange = state.params.model?.base !== newBase; + + if (didBaseModelChange) { + // we may need to reset some incompatible submodels + let modelsUpdatedDisabledOrCleared = 0; + + // handle incompatible loras + state.loras.loras.forEach((lora) => { + if (lora.model.base !== newBase) { + dispatch(loraIsEnabledChanged({ id: lora.id, isEnabled: false })); + modelsUpdatedDisabledOrCleared += 1; + } + }); + + // handle incompatible vae + const { vae } = state.params; + if (vae && vae.base !== newBase) { + dispatch(vaeSelected(null)); + modelsUpdatedDisabledOrCleared += 1; + } + + // handle incompatible Z-Image models - clear if switching away from z-image + const { zImageVaeModel, zImageQwen3EncoderModel, zImageQwen3SourceModel } = state.params; + if (newBase !== 'z-image') { + if (zImageVaeModel) { + dispatch(zImageVaeModelSelected(null)); + modelsUpdatedDisabledOrCleared += 1; + } + if (zImageQwen3EncoderModel) { + dispatch(zImageQwen3EncoderModelSelected(null)); + modelsUpdatedDisabledOrCleared += 1; + } + if (zImageQwen3SourceModel) { + dispatch(zImageQwen3SourceModelSelected(null)); + modelsUpdatedDisabledOrCleared += 1; + } + } else { + // Switching to Z-Image - set defaults if no valid configuration exists + const hasValidConfig = zImageQwen3SourceModel || (zImageVaeModel && zImageQwen3EncoderModel); + + if (!hasValidConfig) { + // Prefer Qwen3 Source (Diffusers model) if available + const availableZImageDiffusers = selectZImageDiffusersModels(state); + + if (availableZImageDiffusers.length > 0) { + const diffusersModel = availableZImageDiffusers[0]; + if (diffusersModel) { + dispatch( + zImageQwen3SourceModelSelected({ + key: diffusersModel.key, + hash: diffusersModel.hash, + name: diffusersModel.name, + base: diffusersModel.base, + type: diffusersModel.type, + }) + ); + } + } else { + // Fallback: try to set Qwen3 Encoder + VAE + const availableQwen3Encoders = selectQwen3EncoderModels(state); + const availableFluxVAEs = selectFluxVAEModels(state); + + if (availableQwen3Encoders.length > 0 && availableFluxVAEs.length > 0) { + const qwen3Encoder = availableQwen3Encoders[0]; + const fluxVAE = availableFluxVAEs[0]; + + if (qwen3Encoder) { + dispatch( + zImageQwen3EncoderModelSelected({ + key: qwen3Encoder.key, + name: qwen3Encoder.name, + base: qwen3Encoder.base, + }) + ); + } + if (fluxVAE) { + dispatch( + zImageVaeModelSelected({ + key: fluxVAE.key, + hash: fluxVAE.hash, + name: fluxVAE.name, + base: fluxVAE.base, + type: fluxVAE.type, + }) + ); + } + } + } + } + } + + // handle incompatible Anima models - clear if switching away from anima + const { animaVaeModel, animaQwen3EncoderModel } = state.params; + if (newBase !== 'anima') { + if (animaVaeModel) { + dispatch(animaVaeModelSelected(null)); + modelsUpdatedDisabledOrCleared += 1; + } + if (animaQwen3EncoderModel) { + dispatch(animaQwen3EncoderModelSelected(null)); + modelsUpdatedDisabledOrCleared += 1; + } + } else { + // Switching to Anima - set defaults if no valid configuration exists + const hasValidConfig = animaVaeModel && animaQwen3EncoderModel; + + if (!hasValidConfig) { + const availableQwen3Encoders = selectAnimaQwen3EncoderModels(state); + const availableAnimaVAEs = selectAnimaVAEModels(state); + + if (availableQwen3Encoders.length > 0 && availableAnimaVAEs.length > 0) { + const qwen3Encoder = availableQwen3Encoders[0]; + const fluxVAE = availableAnimaVAEs[0]; + + if (qwen3Encoder && !animaQwen3EncoderModel) { + dispatch( + animaQwen3EncoderModelSelected({ + key: qwen3Encoder.key, + hash: qwen3Encoder.hash, + name: qwen3Encoder.name, + base: qwen3Encoder.base, + type: qwen3Encoder.type, + }) + ); + } + if (fluxVAE && !animaVaeModel) { + dispatch( + animaVaeModelSelected({ + key: fluxVAE.key, + hash: fluxVAE.hash, + name: fluxVAE.name, + base: fluxVAE.base, + type: fluxVAE.type, + }) + ); + } + } + } + } + + // handle incompatible FLUX.2 Klein models - clear if switching away from flux2 + const { kleinVaeModel, kleinQwen3EncoderModel } = state.params; + if (newBase !== 'flux2') { + if (kleinVaeModel) { + dispatch(kleinVaeModelSelected(null)); + modelsUpdatedDisabledOrCleared += 1; + } + if (kleinQwen3EncoderModel) { + dispatch(kleinQwen3EncoderModelSelected(null)); + modelsUpdatedDisabledOrCleared += 1; + } + } + + // handle incompatible Qwen Image Edit component source - clear if switching away + const { qwenImageComponentSource, qwenImageVaeModel, qwenImageQwenVLEncoderModel } = state.params; + if (newBase !== 'qwen-image') { + if (qwenImageComponentSource) { + dispatch(qwenImageComponentSourceSelected(null)); + modelsUpdatedDisabledOrCleared += 1; + } + if (qwenImageVaeModel) { + dispatch(qwenImageVaeModelSelected(null)); + modelsUpdatedDisabledOrCleared += 1; + } + if (qwenImageQwenVLEncoderModel) { + dispatch(qwenImageQwenVLEncoderModelSelected(null)); + modelsUpdatedDisabledOrCleared += 1; + } + } else { + // Switching to Qwen Image - auto-default component source to a matching diffusers model + if (!qwenImageComponentSource) { + const availableQwenImageDiffusers = selectQwenImageDiffusersModels(state); + + // Look up the new model's variant to match generate vs edit + const modelConfigsResult = selectModelConfigsQuery(state); + let selectedVariant: string | null = null; + if (modelConfigsResult.data) { + const newModelConfig = modelConfigsAdapterSelectors.selectById(modelConfigsResult.data, newModel.key); + if (newModelConfig && 'variant' in newModelConfig && typeof newModelConfig.variant === 'string') { + selectedVariant = newModelConfig.variant; + } + } + + // Find a diffusers model matching the variant; if no variant on denoiser, prefer "generate" then "edit" + const variantToMatch = selectedVariant ?? 'generate'; + const matchingModel = availableQwenImageDiffusers.find( + (m) => 'variant' in m && m.variant === variantToMatch + ); + const fallbackModel = availableQwenImageDiffusers.find( + (m) => 'variant' in m && m.variant !== variantToMatch + ); + const diffusersModel = matchingModel ?? fallbackModel ?? availableQwenImageDiffusers[0]; + + if (diffusersModel) { + dispatch(qwenImageComponentSourceSelected(zModelIdentifierField.parse(diffusersModel))); + } + } + + // Auto-select standalone VAE and Qwen2.5-VL Encoder if available - this allows GGUF + // users to be ready-to-go after installing the starter pack without having to dig into + // Advanced. Only set if the user hasn't already chosen one. + if (!qwenImageVaeModel) { + const availableQwenImageVAEs = selectQwenImageVAEModels(state); + const vae = availableQwenImageVAEs[0]; + if (vae) { + dispatch(qwenImageVaeModelSelected(zModelIdentifierField.parse(vae))); + } + } + if (!qwenImageQwenVLEncoderModel) { + const availableQwenVLEncoders = selectQwenVLEncoderModels(state); + // Prefer diffusers (folder) format over single-file checkpoints, since the latter + // can fail to load on some checkpoints. + const encoder = + availableQwenVLEncoders.find((m) => m.format === 'qwen_vl_encoder') ?? availableQwenVLEncoders[0]; + if (encoder) { + dispatch(qwenImageQwenVLEncoderModelSelected(zModelIdentifierField.parse(encoder))); + } + } + } + + if (newModel.base !== 'external' && SUPPORTS_REF_IMAGES_BASE_MODELS.includes(newModel.base)) { + // Handle incompatible reference image models - switch to first compatible model, with some smart logic + // to choose the best available model based on the new main model. + const allRefImageModels = selectGlobalRefImageModels(state).filter(({ base }) => base === newBase); + + let newGlobalRefImageModel: IPAdapterModelConfig | FLUXKontextModelConfig | FLUXReduxModelConfig | null = + null; + + // Certain models require the ref image model to be the same as the main model - others just need a matching + // base. Helper to grab the first exact match or the first available model if no exact match is found. + const exactMatchOrFirst = ( + candidates: T[] + ): T | null => candidates.find(({ key }) => key === newModel.key) ?? candidates[0] ?? null; + + // The only way we can differentiate between FLUX and FLUX Kontext is to check for "kontext" in the name + if (newModel.base === 'flux' && newModel.name.toLowerCase().includes('kontext')) { + const fluxKontextDevModels = allRefImageModels.filter(isFluxKontextModelConfig); + newGlobalRefImageModel = exactMatchOrFirst(fluxKontextDevModels); + } else if (newModel.base === 'flux') { + const fluxReduxModels = allRefImageModels.filter(isFluxReduxModelConfig); + newGlobalRefImageModel = fluxReduxModels[0] ?? null; + } else { + newGlobalRefImageModel = allRefImageModels[0] ?? null; + } + + // All ref image entities are updated to use the same new model + const refImageEntities = selectReferenceImageEntities(state); + for (const entity of refImageEntities) { + if (newBase === 'flux2') { + // Switching TO FLUX.2 - convert any non-flux2 configs to flux2_reference_image + if (!isFlux2ReferenceImageConfig(entity.config)) { + dispatch( + refImageConfigChanged({ + id: entity.id, + config: { ...initialFlux2ReferenceImage }, + }) + ); + modelsUpdatedDisabledOrCleared += 1; + } + continue; + } + + if (newBase === 'qwen-image') { + // Switching TO Qwen Image Edit - convert any non-qwen configs to qwen_image_reference_image + if (!isQwenImageReferenceImageConfig(entity.config)) { + dispatch( + refImageConfigChanged({ + id: entity.id, + config: { ...initialQwenImageReferenceImage }, + }) + ); + modelsUpdatedDisabledOrCleared += 1; + } + continue; + } + + if (isFlux2ReferenceImageConfig(entity.config)) { + // Switching AWAY from FLUX.2 - convert flux2_reference_image to the appropriate config type + let newConfig; + if (newGlobalRefImageModel) { + const parsedModel = zModelIdentifierField.parse(newGlobalRefImageModel); + if (newModel.base === 'flux' && newModel.name.toLowerCase().includes('kontext')) { + newConfig = { ...initialFluxKontextReferenceImage, model: parsedModel }; + } else if (newGlobalRefImageModel.type === 'flux_redux') { + newConfig = { ...initialFLUXRedux, model: parsedModel }; + } else { + newConfig = { ...initialIPAdapter, model: parsedModel }; + if (parsedModel.base === 'flux') { + newConfig.clipVisionModel = 'ViT-L'; + } + } + } else { + // No compatible model found - fall back to an empty IP adapter config + newConfig = { ...initialIPAdapter }; + } + dispatch(refImageConfigChanged({ id: entity.id, config: newConfig })); + modelsUpdatedDisabledOrCleared += 1; + continue; + } + + if (isQwenImageReferenceImageConfig(entity.config)) { + // Switching AWAY from Qwen Image Edit - convert to the appropriate config type + let newConfig; + if (newGlobalRefImageModel) { + const parsedModel = zModelIdentifierField.parse(newGlobalRefImageModel); + if (newModel.base === 'flux' && newModel.name.toLowerCase().includes('kontext')) { + newConfig = { ...initialFluxKontextReferenceImage, model: parsedModel }; + } else if (newGlobalRefImageModel.type === 'flux_redux') { + newConfig = { ...initialFLUXRedux, model: parsedModel }; + } else { + newConfig = { ...initialIPAdapter, model: parsedModel }; + if (parsedModel.base === 'flux') { + newConfig.clipVisionModel = 'ViT-L'; + } + } + } else { + // No compatible model found - fall back to an empty IP adapter config + newConfig = { ...initialIPAdapter }; + } + dispatch(refImageConfigChanged({ id: entity.id, config: newConfig })); + modelsUpdatedDisabledOrCleared += 1; + continue; + } + + // Standard handling for non-flux2 configs + const shouldUpdateModel = + (entity.config.model && entity.config.model.base !== newBase) || + (!entity.config.model && newGlobalRefImageModel); + + if (shouldUpdateModel) { + dispatch( + refImageModelChanged({ + id: entity.id, + modelConfig: newGlobalRefImageModel, + }) + ); + modelsUpdatedDisabledOrCleared += 1; + } + } + } + + // For regional guidance, there is no smart logic - we just pick the first available model. + const newRegionalRefImageModel = selectRegionalRefImageModels(state)[0] ?? null; + + // All regional guidance entities are updated to use the same new model. + const canvasState = selectCanvasSlice(state); + const canvasRegionalGuidanceEntities = selectAllEntitiesOfType(canvasState, 'regional_guidance'); + for (const entity of canvasRegionalGuidanceEntities) { + for (const refImage of entity.referenceImages) { + // Only change the model if the current one is not compatible with the new base model. + const shouldUpdateModel = + (refImage.config.model && refImage.config.model.base !== newBase) || + (!refImage.config.model && newRegionalRefImageModel); + + if (shouldUpdateModel) { + dispatch( + rgRefImageModelChanged({ + entityIdentifier: getEntityIdentifier(entity), + referenceImageId: refImage.id, + modelConfig: newRegionalRefImageModel, + }) + ); + modelsUpdatedDisabledOrCleared += 1; + } + } + } + + if (modelsUpdatedDisabledOrCleared > 0) { + toast({ + id: 'BASE_MODEL_CHANGED', + title: t('toast.baseModelChanged'), + description: t('toast.baseModelChangedCleared', { + count: modelsUpdatedDisabledOrCleared, + }), + status: 'warning', + }); + } + } + + // Handle FLUX.2 Klein model changes within the same base (different variants need different encoders) + // Clear the Qwen3 encoder only when switching between different Klein variants + // (e.g., klein_4b needs qwen3_4b, klein_9b needs qwen3_8b) + if (newBase === 'flux2' && state.params.model?.base === 'flux2' && newModel.key !== state.params.model?.key) { + const { kleinQwen3EncoderModel } = state.params; + if (kleinQwen3EncoderModel) { + // Get model configs to compare variants + const modelConfigsResult = selectModelConfigsQuery(state); + if (modelConfigsResult.data) { + const oldModelConfig = modelConfigsAdapterSelectors.selectById( + modelConfigsResult.data, + state.params.model.key + ); + const newModelConfig = modelConfigsAdapterSelectors.selectById(modelConfigsResult.data, newModel.key); + + // Extract variants (only clear if variants are different) + const oldVariant = oldModelConfig && 'variant' in oldModelConfig ? oldModelConfig.variant : null; + const newVariant = newModelConfig && 'variant' in newModelConfig ? newModelConfig.variant : null; + + if (oldVariant !== newVariant) { + dispatch(kleinQwen3EncoderModelSelected(null)); + toast({ + id: 'KLEIN_ENCODER_CLEARED', + title: t('toast.kleinEncoderCleared'), + description: t('toast.kleinEncoderClearedDescription'), + status: 'info', + }); + } + } + } + } + + // Handle Qwen Image model changes within the same base (variant may change between generate/edit) + // Auto-update the component source diffusers model to match the new variant + if ( + newBase === 'qwen-image' && + state.params.model?.base === 'qwen-image' && + newModel.key !== state.params.model?.key + ) { + const modelConfigsResult = selectModelConfigsQuery(state); + if (modelConfigsResult.data) { + const newModelConfig = modelConfigsAdapterSelectors.selectById(modelConfigsResult.data, newModel.key); + const newVariant = + newModelConfig && 'variant' in newModelConfig && typeof newModelConfig.variant === 'string' + ? newModelConfig.variant + : 'generate'; + + const availableQwenImageDiffusers = selectQwenImageDiffusersModels(state); + const matchingModel = availableQwenImageDiffusers.find((m) => 'variant' in m && m.variant === newVariant); + const fallbackModel = availableQwenImageDiffusers.find((m) => 'variant' in m && m.variant !== newVariant); + const diffusersModel = matchingModel ?? fallbackModel ?? availableQwenImageDiffusers[0]; + + if (diffusersModel) { + dispatch(qwenImageComponentSourceSelected(zModelIdentifierField.parse(diffusersModel))); + } + } + } + + // Handle Z-Image scheduler when switching to Z-Image Base (zbase) model + // LCM is not supported for undistilled models, so reset to euler + if (newBase === 'z-image' && state.params.zImageScheduler === 'lcm') { + const modelConfigsResult = selectModelConfigsQuery(state); + if (modelConfigsResult.data) { + const newModelConfig = modelConfigsAdapterSelectors.selectById(modelConfigsResult.data, newModel.key); + if (newModelConfig && 'variant' in newModelConfig && newModelConfig.variant === 'zbase') { + dispatch(setZImageScheduler('euler')); + toast({ + id: 'ZIMAGE_SCHEDULER_RESET', + title: t('toast.schedulerReset'), + description: t('toast.schedulerResetZImageBase'), + status: 'info', + }); + } + } + } + + dispatch(modelChanged({ model: newModel, previousModel: state.params.model })); + + const modelBase = selectBboxModelBase(state); + + if (modelBase !== state.params.model?.base) { + // Sync generate tab settings whenever the model base changes + dispatch(syncedToOptimalDimension()); + const isStaging = buildSelectIsStaging(selectCanvasSessionId(state))(state); + if (!isStaging) { + // Canvas tab only syncs if not staging + dispatch(bboxSyncedToOptimalDimension()); + } + } + + // When switching to an external model, sync bbox to the model's first preset dimensions + if (newBase === 'external') { + const modelConfigsResult = selectModelConfigsQuery(getState()); + if (modelConfigsResult.data) { + const newModelConfig = modelConfigsAdapterSelectors.selectById(modelConfigsResult.data, newModel.key); + if (newModelConfig && isExternalApiModelConfig(newModelConfig)) { + const { aspect_ratio_sizes, resolution_presets } = newModelConfig.capabilities; + if (resolution_presets && resolution_presets.length > 0) { + const firstPreset = resolution_presets[0]!; + dispatch( + resolutionPresetSelected({ + imageSize: firstPreset.image_size, + aspectRatio: firstPreset.aspect_ratio, + width: firstPreset.width, + height: firstPreset.height, + }) + ); + } else if (aspect_ratio_sizes) { + const firstRatio = Object.keys(aspect_ratio_sizes)[0]; + const firstSize = firstRatio ? aspect_ratio_sizes[firstRatio] : undefined; + if (firstRatio && firstSize && isAspectRatioID(firstRatio)) { + dispatch(aspectRatioIdChanged({ id: firstRatio, fixedSize: firstSize })); + } + } + } + } + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts new file mode 100644 index 00000000000..8cbbc72343b --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -0,0 +1,468 @@ +import { logger } from 'app/logging/logger'; +import type { AppDispatch, AppStartListening, RootState } from 'app/store/store'; +import { controlLayerModelChanged, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice'; +import { loraDeleted } from 'features/controlLayers/store/lorasSlice'; +import { + clipEmbedModelSelected, + fluxVAESelected, + modelChanged, + refinerModelChanged, + t5EncoderModelSelected, + vaeSelected, +} from 'features/controlLayers/store/paramsSlice'; +import { refImageModelChanged, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice'; +import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; +import { + getEntityIdentifier, + isFLUXReduxConfig, + isIPAdapterConfig, + isRegionalGuidanceFLUXReduxConfig, + isRegionalGuidanceIPAdapterConfig, +} from 'features/controlLayers/store/types'; +import { modelSelected } from 'features/parameters/store/actions'; +import { + postProcessingModelChanged, + tileControlnetModelChanged, + upscaleModelChanged, +} from 'features/parameters/store/upscaleSlice'; +import { + zParameterCLIPEmbedModel, + zParameterSpandrelImageToImageModel, + zParameterT5EncoderModel, + zParameterVAEModel, +} from 'features/parameters/types/parameterSchemas'; +import type { Logger } from 'roarr'; +import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; +import type { AnyModelConfig } from 'services/api/types'; +import { + isCLIPEmbedModelConfigOrSubmodel, + isControlLayerModelConfig, + isControlNetModelConfig, + isFluxReduxModelConfig, + isFluxVAEModelConfig, + isIPAdapterModelConfig, + isLoRAModelConfig, + isNonFluxVAEModelConfig, + isNonRefinerMainModelConfig, + isRefinerMainModelModelConfig, + isSpandrelImageToImageModelConfig, + isT5EncoderModelConfigOrSubmodel, +} from 'services/api/types'; +import type { JsonObject } from 'type-fest'; + +const log = logger('models'); + +/** + * This listener handles resetting or selecting models as we receive the big list of models from the API. + * + * For example, if a selected model is no longer available, it resets that models selection in redux. + * + * Or, if the model selection is one that should always be populated if possible, like main models, the listener + * attempts to populate it. + * + * Some models, like VAEs, are optional and can be `null` - this listener will only clear the selection if the model is + * no longer available, it will not attempt to select a new model. + */ +export const addModelsLoadedListener = (startAppListening: AppStartListening) => { + startAppListening({ + predicate: modelsApi.endpoints.getModelConfigs.matchFulfilled, + effect: (action, { getState, dispatch }) => { + // models loaded, we need to ensure the selected model is available and if not, select the first one + log.info({ models: action.payload.entities }, `Models loaded (${action.payload.ids.length})`); + + const state = getState(); + + const models = modelConfigsAdapterSelectors.selectAll(action.payload); + + handleMainModels(models, state, dispatch, log); + handleRefinerModels(models, state, dispatch, log); + handleVAEModels(models, state, dispatch, log); + handleLoRAModels(models, state, dispatch, log); + handleControlAdapterModels(models, state, dispatch, log); + handlePostProcessingModel(models, state, dispatch, log); + handleUpscaleModel(models, state, dispatch, log); + handleTileControlNetModel(models, state, dispatch, log); + handleIPAdapterModels(models, state, dispatch, log); + handleT5EncoderModels(models, state, dispatch, log); + handleCLIPEmbedModels(models, state, dispatch, log); + handleFLUXVAEModels(models, state, dispatch, log); + handleFLUXReduxModels(models, state, dispatch, log); + }, + }); +}; + +type ModelHandler = ( + models: AnyModelConfig[], + state: RootState, + dispatch: AppDispatch, + log: Logger +) => undefined; + +const handleMainModels: ModelHandler = (models, state, dispatch, log) => { + const selectedMainModel = state.params.model; + const allMainModels = models.filter(isNonRefinerMainModelConfig).sort((a) => (a.base === 'sdxl' ? -1 : 1)); + + const firstModel = allMainModels[0]; + + // If we have no models, we may need to clear the selected model + if (!firstModel) { + // Only clear the model if we have one currently selected + if (selectedMainModel !== null) { + log.debug({ selectedMainModel }, 'No main models available, clearing'); + dispatch(modelChanged({ model: null })); + } + return; + } + + // If the current model is available, we don't need to do anything + if (allMainModels.some((m) => m.key === selectedMainModel?.key)) { + return; + } + + log.debug( + { selectedMainModel, firstModel }, + 'No selected main model or selected main model is not available, selecting first available model' + ); + dispatch(modelSelected(firstModel)); +}; + +const handleRefinerModels: ModelHandler = (models, state, dispatch, log) => { + const selectedRefinerModel = state.params.refinerModel; + + // `null` is a valid refiner model - no need to do anything. + if (selectedRefinerModel === null) { + return; + } + + // We have a refiner model selected, need to check if it is available + + // Grab just the refiner models + const allRefinerModels = models.filter(isRefinerMainModelModelConfig); + + // If the current refiner model is available, we don't need to do anything + if (allRefinerModels.some((m) => m.key === selectedRefinerModel.key)) { + return; + } + + // Else, we need to clear the refiner model + log.debug({ selectedRefinerModel }, 'Selected refiner model is not available, clearing'); + dispatch(refinerModelChanged(null)); + return; +}; + +const handleVAEModels: ModelHandler = (models, state, dispatch, log) => { + const selectedVAEModel = state.params.vae; + + // `null` is a valid VAE - it means "use the VAE baked into the currently-selected main model" + if (selectedVAEModel === null) { + return; + } + + // We have a VAE selected, need to check if it is available + + // Grab just the VAE models + const vaeModels = models.filter((m) => isNonFluxVAEModelConfig(m)); + + // If the current VAE model is available, we don't need to do anything + if (vaeModels.some((m) => m.key === selectedVAEModel.key)) { + return; + } + + // Else, we need to clear the VAE model + log.debug({ selectedVAEModel }, 'Selected VAE model is not available, clearing'); + dispatch(vaeSelected(null)); + return; +}; + +const handleLoRAModels: ModelHandler = (models, state, dispatch, log) => { + const loraModels = models.filter(isLoRAModelConfig); + state.loras.loras.forEach((lora) => { + const isLoRAAvailable = loraModels.some((m) => m.key === lora.model.key); + if (isLoRAAvailable) { + return; + } + log.debug({ model: lora.model }, 'LoRA model is not available, clearing'); + dispatch(loraDeleted({ id: lora.id })); + }); +}; + +const handleControlAdapterModels: ModelHandler = (models, state, dispatch, log) => { + const caModels = models.filter(isControlLayerModelConfig); + selectCanvasSlice(state).controlLayers.entities.forEach((entity) => { + const selectedControlAdapterModel = entity.controlAdapter.model; + // `null` is a valid control adapter model - no need to do anything. + if (!selectedControlAdapterModel) { + return; + } + const isModelAvailable = caModels.some((m) => m.key === selectedControlAdapterModel.key); + if (isModelAvailable) { + return; + } + log.debug({ selectedControlAdapterModel }, 'Selected control adapter model is not available, clearing'); + dispatch(controlLayerModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null })); + }); +}; + +const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => { + const ipaModels = models.filter(isIPAdapterModelConfig); + selectRefImagesSlice(state).entities.forEach((entity) => { + if (!isIPAdapterConfig(entity.config)) { + return; + } + + const selectedIPAdapterModel = entity.config.model; + // `null` is a valid IP adapter model - no need to do anything. + if (!selectedIPAdapterModel) { + return; + } + const isModelAvailable = ipaModels.some((m) => m.key === selectedIPAdapterModel.key); + if (isModelAvailable) { + return; + } + log.debug({ selectedIPAdapterModel }, 'Selected IP adapter model is not available, clearing'); + dispatch(refImageModelChanged({ id: entity.id, modelConfig: null })); + }); + + selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => { + entity.referenceImages.forEach(({ id: referenceImageId, config }) => { + if (!isRegionalGuidanceIPAdapterConfig(config)) { + return; + } + + const selectedIPAdapterModel = config.model; + // `null` is a valid IP adapter model - no need to do anything. + if (!selectedIPAdapterModel) { + return; + } + const isModelAvailable = ipaModels.some((m) => m.key === selectedIPAdapterModel.key); + if (isModelAvailable) { + return; + } + log.debug({ selectedIPAdapterModel }, 'Selected IP adapter model is not available, clearing'); + dispatch( + rgRefImageModelChanged({ entityIdentifier: getEntityIdentifier(entity), referenceImageId, modelConfig: null }) + ); + }); + }); +}; + +const handleFLUXReduxModels: ModelHandler = (models, state, dispatch, log) => { + const fluxReduxModels = models.filter(isFluxReduxModelConfig); + + selectRefImagesSlice(state).entities.forEach((entity) => { + if (!isFLUXReduxConfig(entity.config)) { + return; + } + const selectedFLUXReduxModel = entity.config.model; + // `null` is a valid FLUX Redux model - no need to do anything. + if (!selectedFLUXReduxModel) { + return; + } + const isModelAvailable = fluxReduxModels.some((m) => m.key === selectedFLUXReduxModel.key); + if (isModelAvailable) { + return; + } + log.debug({ selectedFLUXReduxModel }, 'Selected FLUX Redux model is not available, clearing'); + dispatch(refImageModelChanged({ id: entity.id, modelConfig: null })); + }); + + selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => { + entity.referenceImages.forEach(({ id: referenceImageId, config }) => { + if (!isRegionalGuidanceFLUXReduxConfig(config)) { + return; + } + + const selectedFLUXReduxModel = config.model; + // `null` is a valid FLUX Redux model - no need to do anything. + if (!selectedFLUXReduxModel) { + return; + } + const isModelAvailable = fluxReduxModels.some((m) => m.key === selectedFLUXReduxModel.key); + if (isModelAvailable) { + return; + } + log.debug({ selectedFLUXReduxModel }, 'Selected FLUX Redux model is not available, clearing'); + dispatch( + rgRefImageModelChanged({ entityIdentifier: getEntityIdentifier(entity), referenceImageId, modelConfig: null }) + ); + }); + }); +}; + +const handlePostProcessingModel: ModelHandler = (models, state, dispatch, log) => { + const selectedPostProcessingModel = state.upscale.postProcessingModel; + const allSpandrelModels = models.filter(isSpandrelImageToImageModelConfig); + + // If the currently selected model is available, we don't need to do anything + if (selectedPostProcessingModel && allSpandrelModels.some((m) => m.key === selectedPostProcessingModel.key)) { + return; + } + + // Else we should select the first available model + const firstModel = allSpandrelModels[0] || null; + if (firstModel) { + log.debug( + { selectedPostProcessingModel, firstModel }, + 'No selected post-processing model or selected post-processing model is not available, selecting first available model' + ); + dispatch(postProcessingModelChanged(zParameterSpandrelImageToImageModel.parse(firstModel))); + return; + } + + // No available models, we should clear the selected model - but only if we have one selected + if (selectedPostProcessingModel) { + log.debug({ selectedPostProcessingModel }, 'Selected post-processing model is not available, clearing'); + dispatch(postProcessingModelChanged(null)); + } +}; + +const handleUpscaleModel: ModelHandler = (models, state, dispatch, log) => { + const selectedUpscaleModel = state.upscale.upscaleModel; + const allSpandrelModels = models.filter(isSpandrelImageToImageModelConfig); + + // If the currently selected model is available, we don't need to do anything + if (selectedUpscaleModel && allSpandrelModels.some((m) => m.key === selectedUpscaleModel.key)) { + return; + } + + // Else we should select the first available model + const firstModel = allSpandrelModels[0] || null; + if (firstModel) { + log.debug( + { selectedUpscaleModel, firstModel }, + 'No selected upscale model or selected upscale model is not available, selecting first available model' + ); + dispatch(upscaleModelChanged(zParameterSpandrelImageToImageModel.parse(firstModel))); + return; + } + + // No available models, we should clear the selected model - but only if we have one selected + if (selectedUpscaleModel) { + log.debug({ selectedUpscaleModel }, 'Selected upscale model is not available, clearing'); + dispatch(upscaleModelChanged(null)); + } +}; + +const handleTileControlNetModel: ModelHandler = (models, state, dispatch, log) => { + const selectedTileControlNetModel = state.upscale.tileControlnetModel; + const controlNetModels = models.filter(isControlNetModelConfig); + + // If the currently selected model is available, we don't need to do anything + if (selectedTileControlNetModel && controlNetModels.some((m) => m.key === selectedTileControlNetModel.key)) { + return; + } + + // The only way we have to identify a model as a tile model is by its name containing 'tile' :) + const tileModel = controlNetModels.find((m) => m.name.toLowerCase().includes('tile')); + + // If we have a tile model, select it + if (tileModel) { + log.debug( + { selectedTileControlNetModel, tileModel }, + 'No selected tile ControlNet model or selected model is not available, selecting tile model' + ); + dispatch(tileControlnetModelChanged(tileModel)); + return; + } + + // Otherwise, select the first available ControlNet model + const firstModel = controlNetModels[0] || null; + if (firstModel) { + log.debug( + { selectedTileControlNetModel, firstModel }, + 'No tile ControlNet model found, selecting first available ControlNet model' + ); + dispatch(tileControlnetModelChanged(firstModel)); + return; + } + + // No available models, we should clear the selected model - but only if we have one selected + if (selectedTileControlNetModel) { + log.debug({ selectedTileControlNetModel }, 'Selected tile ControlNet model is not available, clearing'); + dispatch(tileControlnetModelChanged(null)); + } +}; + +const handleT5EncoderModels: ModelHandler = (models, state, dispatch, log) => { + const selectedT5EncoderModel = state.params.t5EncoderModel; + const t5EncoderModels = models.filter((m) => isT5EncoderModelConfigOrSubmodel(m)); + + // If the currently selected model is available, we don't need to do anything + if (selectedT5EncoderModel && t5EncoderModels.some((m) => m.key === selectedT5EncoderModel.key)) { + return; + } + + // Else we should select the first available model + const firstModel = t5EncoderModels[0] || null; + if (firstModel) { + log.debug( + { selectedT5EncoderModel, firstModel }, + 'No selected T5 encoder model or selected T5 encoder model is not available, selecting first available model' + ); + dispatch(t5EncoderModelSelected(zParameterT5EncoderModel.parse(firstModel))); + return; + } + + // No available models, we should clear the selected model - but only if we have one selected + if (selectedT5EncoderModel) { + log.debug({ selectedT5EncoderModel }, 'Selected T5 encoder model is not available, clearing'); + dispatch(t5EncoderModelSelected(null)); + return; + } +}; + +const handleCLIPEmbedModels: ModelHandler = (models, state, dispatch, log) => { + const selectedCLIPEmbedModel = state.params.clipEmbedModel; + const CLIPEmbedModels = models.filter((m) => isCLIPEmbedModelConfigOrSubmodel(m)); + + // If the currently selected model is available, we don't need to do anything + if (selectedCLIPEmbedModel && CLIPEmbedModels.some((m) => m.key === selectedCLIPEmbedModel.key)) { + return; + } + + // Else we should select the first available model + const firstModel = CLIPEmbedModels[0] || null; + if (firstModel) { + log.debug( + { selectedCLIPEmbedModel, firstModel }, + 'No selected CLIP embed model or selected CLIP embed model is not available, selecting first available model' + ); + dispatch(clipEmbedModelSelected(zParameterCLIPEmbedModel.parse(firstModel))); + return; + } + + // No available models, we should clear the selected model - but only if we have one selected + if (selectedCLIPEmbedModel) { + log.debug({ selectedCLIPEmbedModel }, 'Selected CLIP embed model is not available, clearing'); + dispatch(clipEmbedModelSelected(null)); + return; + } +}; + +const handleFLUXVAEModels: ModelHandler = (models, state, dispatch, log) => { + const selectedFLUXVAEModel = state.params.fluxVAE; + const fluxVAEModels = models.filter((m) => isFluxVAEModelConfig(m)); + + // If the currently selected model is available, we don't need to do anything + if (selectedFLUXVAEModel && fluxVAEModels.some((m) => m.key === selectedFLUXVAEModel.key)) { + return; + } + + // Else we should select the first available model + const firstModel = fluxVAEModels[0] || null; + if (firstModel) { + log.debug( + { selectedFLUXVAEModel, firstModel }, + 'No selected FLUX VAE model or selected FLUX VAE model is not available, selecting first available model' + ); + dispatch(fluxVAESelected(zParameterVAEModel.parse(firstModel))); + return; + } + + // No available models, we should clear the selected model - but only if we have one selected + if (selectedFLUXVAEModel) { + log.debug({ selectedFLUXVAEModel }, 'Selected FLUX VAE model is not available, clearing'); + dispatch(fluxVAESelected(null)); + return; + } +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts new file mode 100644 index 00000000000..1ebad3a0694 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts @@ -0,0 +1,145 @@ +import type { AppStartListening } from 'app/store/store'; +import { isNil } from 'es-toolkit'; +import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice'; +import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { + heightChanged, + setCfgRescaleMultiplier, + setCfgScale, + setGuidance, + setScheduler, + setSteps, + vaePrecisionChanged, + vaeSelected, + widthChanged, +} from 'features/controlLayers/store/paramsSlice'; +import { setDefaultSettings } from 'features/parameters/store/actions'; +import { + isParameterCFGRescaleMultiplier, + isParameterCFGScale, + isParameterGuidance, + isParameterHeight, + isParameterPrecision, + isParameterScheduler, + isParameterSteps, + isParameterWidth, + zParameterVAEModel, +} from 'features/parameters/types/parameterSchemas'; +import { toast } from 'features/toast/toast'; +import { selectActiveTab } from 'features/ui/store/uiSelectors'; +import { t } from 'i18next'; +import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; +import { isNonRefinerMainModelConfig } from 'services/api/types'; + +export const addSetDefaultSettingsListener = (startAppListening: AppStartListening) => { + startAppListening({ + actionCreator: setDefaultSettings, + effect: async (action, { dispatch, getState }) => { + const state = getState(); + + const currentModel = state.params.model; + + if (!currentModel) { + return; + } + + const request = dispatch(modelsApi.endpoints.getModelConfigs.initiate(undefined, { subscribe: false })); + const data = await request.unwrap(); + const models = modelConfigsAdapterSelectors.selectAll(data); + + const modelConfig = models.find((model) => model.key === currentModel.key); + + if (!modelConfig) { + return; + } + + if (isNonRefinerMainModelConfig(modelConfig) && modelConfig.default_settings) { + const { vae, vae_precision, cfg_scale, cfg_rescale_multiplier, steps, scheduler, width, height, guidance } = + modelConfig.default_settings; + + if (vae) { + // we store this as "default" within default settings + // to distinguish it from no default set + if (vae === 'default') { + dispatch(vaeSelected(null)); + } else { + const vaeModel = models.find((model) => model.key === vae); + const result = zParameterVAEModel.safeParse(vaeModel); + if (!result.success) { + return; + } + dispatch(vaeSelected(result.data)); + } + } + + if (vae_precision) { + if (isParameterPrecision(vae_precision)) { + dispatch(vaePrecisionChanged(vae_precision)); + } + } + + if (guidance) { + if (isParameterGuidance(guidance)) { + dispatch(setGuidance(guidance)); + } + } + + if (!isNil(cfg_scale)) { + if (isParameterCFGScale(cfg_scale)) { + dispatch(setCfgScale(cfg_scale)); + } + } + + if (!isNil(cfg_rescale_multiplier)) { + if (isParameterCFGRescaleMultiplier(cfg_rescale_multiplier)) { + dispatch(setCfgRescaleMultiplier(cfg_rescale_multiplier)); + } + } else { + // Set this to 0 if it doesn't have a default. This value is + // easy to miss in the UI when users are resetting defaults + // and leaving it non-zero could lead to detrimental + // effects. + dispatch(setCfgRescaleMultiplier(0)); + } + + if (steps) { + if (isParameterSteps(steps)) { + dispatch(setSteps(steps)); + } + } + + if (scheduler) { + if (isParameterScheduler(scheduler)) { + dispatch(setScheduler(scheduler)); + } + } + const setSizeOptions = { updateAspectRatio: true, clamp: true }; + + const isStaging = buildSelectIsStaging(selectCanvasSessionId(state))(state); + + const activeTab = selectActiveTab(getState()); + if (activeTab === 'generate') { + if (isParameterWidth(width)) { + dispatch(widthChanged({ width, ...setSizeOptions })); + } + if (isParameterHeight(height)) { + dispatch(heightChanged({ height, ...setSizeOptions })); + } + } + + if (activeTab === 'canvas') { + if (!isStaging) { + if (isParameterWidth(width)) { + dispatch(bboxWidthChanged({ width, ...setSizeOptions })); + } + if (isParameterHeight(height)) { + dispatch(bboxHeightChanged({ height, ...setSizeOptions })); + } + } + } + + toast({ id: 'PARAMETER_SET', title: t('toast.parameterSet', { parameter: 'Default settings' }) }); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketConnected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketConnected.ts new file mode 100644 index 00000000000..d89f89104ab --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketConnected.ts @@ -0,0 +1,81 @@ +import { objectEquals } from '@observ33r/object-equals'; +import { createAction } from '@reduxjs/toolkit'; +import { logger } from 'app/logging/logger'; +import type { AppStartListening } from 'app/store/store'; +import { atom } from 'nanostores'; +import { api } from 'services/api'; +import { modelsApi } from 'services/api/endpoints/models'; +import { queueApi, selectQueueStatus } from 'services/api/endpoints/queue'; + +const log = logger('events'); + +const $isFirstConnection = atom(true); +export const socketConnected = createAction('socket/connected'); + +export const addSocketConnectedEventListener = (startAppListening: AppStartListening) => { + startAppListening({ + actionCreator: socketConnected, + effect: async (action, { dispatch, getState }) => { + /** + * The rest of this listener has recovery logic for when the socket disconnects and reconnects. + * + * We need to re-fetch if something has changed while we were disconnected. + * + * Session queue status is one proxy for disconnected changes. Model installs need explicit recovery + * as well because they can transition to paused during backend shutdown while the socket is down. + * + * The queue status is a proxy for this - if the queue status has changed, we need to re-fetch + * the queries that may have changed while we were disconnected. + */ + + // Bail on the recovery logic if this is the first connection - we don't need to recover anything + if ($isFirstConnection.get()) { + // Populate the model configs on first connection. + dispatch(modelsApi.endpoints.getModelConfigs.initiate(undefined, { subscribe: false })); + $isFirstConnection.set(false); + return; + } + + // If we are in development mode, reset the whole API state. In this scenario, reconnects will + // typically be caused by reloading the server, in which case we do want to reset the whole API. + if (import.meta.env.MODE === 'development') { + dispatch(api.util.resetApiState()); + } + + // Always re-sync model installs on reconnect. + dispatch( + modelsApi.endpoints.listModelInstalls.initiate(undefined, { + forceRefetch: true, + subscribe: false, + }) + ); + + // Else, we need to compare the last-known queue status with the current queue status, re-fetching + // everything if it has changed. + const prevQueueStatusData = selectQueueStatus(getState()).data; + + try { + // Fetch the queue status again + const queueStatusRequest = dispatch( + queueApi.endpoints.getQueueStatus.initiate(undefined, { + forceRefetch: true, + subscribe: false, + }) + ); + const nextQueueStatusData = await queueStatusRequest.unwrap(); + + // If the queue hasn't changed, we don't need to do anything. + if (objectEquals(prevQueueStatusData?.queue, nextQueueStatusData.queue)) { + return; + } + + //The queue has changed. We need to re-fetch everything that may have changed while we were + // disconnected. + dispatch(api.util.invalidateTags(['FetchOnReconnect'])); + } catch { + // no-op + log.debug('Unable to get current queue status on reconnect'); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/nanostores/store.ts b/invokeai/frontend/web/src/app/store/nanostores/store.ts new file mode 100644 index 00000000000..d8248b79a0a --- /dev/null +++ b/invokeai/frontend/web/src/app/store/nanostores/store.ts @@ -0,0 +1,33 @@ +import type { AppStore } from 'app/store/store'; +import { atom } from 'nanostores'; + +// Inject socket options and url into window for debugging +declare global { + interface Window { + $store?: typeof $store; + } +} + +/** + * Raised when the redux store is unable to be retrieved. + */ +class ReduxStoreNotInitialized extends Error { + /** + * Create ReduxStoreNotInitialized + * @param {String} message + */ + constructor(message = 'Redux store not initialized') { + super(message); + this.name = this.constructor.name; + } +} + +export const $store = atom>(); + +export const getStore = () => { + const store = $store.get(); + if (!store) { + throw new ReduxStoreNotInitialized(); + } + return store; +}; diff --git a/invokeai/frontend/web/src/app/store/nanostores/util.ts b/invokeai/frontend/web/src/app/store/nanostores/util.ts new file mode 100644 index 00000000000..6797996b217 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/nanostores/util.ts @@ -0,0 +1,16 @@ +import type { ReadableAtom } from 'nanostores'; +import { atom } from 'nanostores'; + +/** + * A fallback non-writable atom that always returns `false`, used when a nanostores atom is only conditionally available + * in a hook or component. + * + */ +export const $false: ReadableAtom = atom(false); +/** + * A fallback non-writable atom that always returns `true`, used when a nanostores atom is only conditionally available + * in a hook or component. + * + * @knipignore + */ +export const $true: ReadableAtom = atom(true); diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts new file mode 100644 index 00000000000..f24d2d0105c --- /dev/null +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -0,0 +1,296 @@ +import type { ThunkDispatch, TypedStartListening, UnknownAction } from '@reduxjs/toolkit'; +import { addListener, combineReducers, configureStore, createAction, createListenerMiddleware } from '@reduxjs/toolkit'; +import { logger } from 'app/logging/logger'; +import { errorHandler } from 'app/store/enhancers/reduxRemember/errors'; +import { addAdHocPostProcessingRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener'; +import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued'; +import { addAppStartedListener } from 'app/store/middleware/listenerMiddleware/listeners/appStarted'; +import { addBatchEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/batchEnqueued'; +import { addDeleteBoardAndImagesFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted'; +import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected'; +import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload'; +import { addGetOpenAPISchemaListener } from 'app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema'; +import { addImageAddedToBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard'; +import { addImageRemovedFromBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard'; +import { addModelSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelSelected'; +import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded'; +import { addSetDefaultSettingsListener } from 'app/store/middleware/listenerMiddleware/listeners/setDefaultSettings'; +import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketConnected'; +import { deepClone } from 'common/util/deepClone'; +import { merge } from 'es-toolkit'; +import { omit, pick } from 'es-toolkit/compat'; +import { authSliceConfig } from 'features/auth/store/authSlice'; +import { changeBoardModalSliceConfig } from 'features/changeBoardModal/store/slice'; +import { canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSettingsSlice'; +import { canvasSliceConfig } from 'features/controlLayers/store/canvasSlice'; +import { canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { canvasTextSliceConfig } from 'features/controlLayers/store/canvasTextSlice'; +import { canvasWorkflowIntegrationSliceConfig } from 'features/controlLayers/store/canvasWorkflowIntegrationSlice'; +import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice'; +import { paramsSliceConfig } from 'features/controlLayers/store/paramsSlice'; +import { refImagesSliceConfig } from 'features/controlLayers/store/refImagesSlice'; +import { dynamicPromptsSliceConfig } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; +import { gallerySliceConfig } from 'features/gallery/store/gallerySlice'; +import { modelManagerSliceConfig } from 'features/modelManagerV2/store/modelManagerV2Slice'; +import { nodesSliceConfig } from 'features/nodes/store/nodesSlice'; +import { workflowLibrarySliceConfig } from 'features/nodes/store/workflowLibrarySlice'; +import { workflowSettingsSliceConfig } from 'features/nodes/store/workflowSettingsSlice'; +import { upscaleSliceConfig } from 'features/parameters/store/upscaleSlice'; +import { queueSliceConfig } from 'features/queue/store/queueSlice'; +import { stylePresetSliceConfig } from 'features/stylePresets/store/stylePresetSlice'; +import { hotkeysSliceConfig } from 'features/system/store/hotkeysSlice'; +import { systemSliceConfig } from 'features/system/store/systemSlice'; +import { uiSliceConfig } from 'features/ui/store/uiSlice'; +import { diff } from 'jsondiffpatch'; +import type { SerializeFunction, UnserializeFunction } from 'redux-remember'; +import { REMEMBER_REHYDRATED, rememberEnhancer, rememberReducer } from 'redux-remember'; +import undoable, { newHistory } from 'redux-undo'; +import { serializeError } from 'serialize-error'; +import { api } from 'services/api'; +import type { JsonObject } from 'type-fest'; + +import { reduxRememberDriver } from './enhancers/reduxRemember/driver'; +import { actionSanitizer } from './middleware/devtools/actionSanitizer'; +import { actionsDenylist } from './middleware/devtools/actionsDenylist'; +import { stateSanitizer } from './middleware/devtools/stateSanitizer'; +import { addArchivedOrDeletedBoardListener } from './middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener'; +import { addPBRFilterListener } from './middleware/listenerMiddleware/listeners/addPBRFilterListener'; +import { addImageUploadedFulfilledListener } from './middleware/listenerMiddleware/listeners/imageUploaded'; + +const listenerMiddleware = createListenerMiddleware(); + +const log = logger('system'); + +// When adding a slice, add the config to the SLICE_CONFIGS object below, then add the reducer to ALL_REDUCERS. +const SLICE_CONFIGS = { + [authSliceConfig.slice.reducerPath]: authSliceConfig, + [canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig, + [canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig, + [canvasTextSliceConfig.slice.reducerPath]: canvasTextSliceConfig, + [canvasSliceConfig.slice.reducerPath]: canvasSliceConfig, + [canvasWorkflowIntegrationSliceConfig.slice.reducerPath]: canvasWorkflowIntegrationSliceConfig, + [changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig, + [dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig, + [gallerySliceConfig.slice.reducerPath]: gallerySliceConfig, + [hotkeysSliceConfig.slice.reducerPath]: hotkeysSliceConfig, + [lorasSliceConfig.slice.reducerPath]: lorasSliceConfig, + [modelManagerSliceConfig.slice.reducerPath]: modelManagerSliceConfig, + [nodesSliceConfig.slice.reducerPath]: nodesSliceConfig, + [paramsSliceConfig.slice.reducerPath]: paramsSliceConfig, + [queueSliceConfig.slice.reducerPath]: queueSliceConfig, + [refImagesSliceConfig.slice.reducerPath]: refImagesSliceConfig, + [stylePresetSliceConfig.slice.reducerPath]: stylePresetSliceConfig, + [systemSliceConfig.slice.reducerPath]: systemSliceConfig, + [uiSliceConfig.slice.reducerPath]: uiSliceConfig, + [upscaleSliceConfig.slice.reducerPath]: upscaleSliceConfig, + [workflowLibrarySliceConfig.slice.reducerPath]: workflowLibrarySliceConfig, + [workflowSettingsSliceConfig.slice.reducerPath]: workflowSettingsSliceConfig, +}; + +// TS makes it really hard to dynamically create this object :/ so it's just hardcoded here. +// Remember to wrap undoable reducers in `undoable()`! +const ALL_REDUCERS = { + [api.reducerPath]: api.reducer, + [authSliceConfig.slice.reducerPath]: authSliceConfig.slice.reducer, + [canvasSessionSliceConfig.slice.reducerPath]: canvasSessionSliceConfig.slice.reducer, + [canvasSettingsSliceConfig.slice.reducerPath]: canvasSettingsSliceConfig.slice.reducer, + [canvasTextSliceConfig.slice.reducerPath]: canvasTextSliceConfig.slice.reducer, + // Undoable! + [canvasSliceConfig.slice.reducerPath]: undoable( + canvasSliceConfig.slice.reducer, + canvasSliceConfig.undoableConfig?.reduxUndoOptions + ), + [canvasWorkflowIntegrationSliceConfig.slice.reducerPath]: canvasWorkflowIntegrationSliceConfig.slice.reducer, + [changeBoardModalSliceConfig.slice.reducerPath]: changeBoardModalSliceConfig.slice.reducer, + [dynamicPromptsSliceConfig.slice.reducerPath]: dynamicPromptsSliceConfig.slice.reducer, + [gallerySliceConfig.slice.reducerPath]: gallerySliceConfig.slice.reducer, + [hotkeysSliceConfig.slice.reducerPath]: hotkeysSliceConfig.slice.reducer, + [lorasSliceConfig.slice.reducerPath]: lorasSliceConfig.slice.reducer, + [modelManagerSliceConfig.slice.reducerPath]: modelManagerSliceConfig.slice.reducer, + // Undoable! + [nodesSliceConfig.slice.reducerPath]: undoable( + nodesSliceConfig.slice.reducer, + nodesSliceConfig.undoableConfig?.reduxUndoOptions + ), + [paramsSliceConfig.slice.reducerPath]: paramsSliceConfig.slice.reducer, + [queueSliceConfig.slice.reducerPath]: queueSliceConfig.slice.reducer, + [refImagesSliceConfig.slice.reducerPath]: refImagesSliceConfig.slice.reducer, + [stylePresetSliceConfig.slice.reducerPath]: stylePresetSliceConfig.slice.reducer, + [systemSliceConfig.slice.reducerPath]: systemSliceConfig.slice.reducer, + [uiSliceConfig.slice.reducerPath]: uiSliceConfig.slice.reducer, + [upscaleSliceConfig.slice.reducerPath]: upscaleSliceConfig.slice.reducer, + [workflowLibrarySliceConfig.slice.reducerPath]: workflowLibrarySliceConfig.slice.reducer, + [workflowSettingsSliceConfig.slice.reducerPath]: workflowSettingsSliceConfig.slice.reducer, +}; + +const rootReducer = combineReducers(ALL_REDUCERS); + +const rememberedRootReducer = rememberReducer(rootReducer); + +const unserialize: UnserializeFunction = (data, key) => { + const sliceConfig = SLICE_CONFIGS[key as keyof typeof SLICE_CONFIGS]; + if (!sliceConfig?.persistConfig) { + throw new Error(`No persist config for slice "${key}"`); + } + const { getInitialState, persistConfig, undoableConfig } = sliceConfig; + let state; + try { + const initialState = getInitialState(); + const parsed = JSON.parse(data); + + // We need to inject non-persisted values from initial state into the rehydrated state. These values always are + // required to be in the state, but won't be in the persisted data. Build an object that consists of only these + // values, then merge it with the rehydrated state. + const nonPersistedSubsetOfState = pick(initialState, persistConfig.persistDenylist ?? []); + const stateToMigrate = merge(deepClone(parsed), nonPersistedSubsetOfState); + + // Run migrations to bring old state up to date with the current version. + const migrated = persistConfig.migrate(stateToMigrate); + + log.debug( + { + persistedData: parsed as JsonObject, + rehydratedData: migrated as JsonObject, + diff: diff(data, migrated) as JsonObject, + }, + `Rehydrated slice "${key}"` + ); + state = migrated; + } catch (err) { + log.warn( + { error: serializeError(err as Error) }, + `Error rehydrating slice "${key}", falling back to default initial state` + ); + state = getInitialState(); + } + + // Undoable slices must be wrapped in a history! + if (undoableConfig) { + return newHistory([], state, []); + } else { + return state; + } +}; + +const serialize: SerializeFunction = (data, key) => { + const sliceConfig = SLICE_CONFIGS[key as keyof typeof SLICE_CONFIGS]; + if (!sliceConfig?.persistConfig) { + throw new Error(`No persist config for slice "${key}"`); + } + + const result = omit( + sliceConfig.undoableConfig ? data.present : data, + sliceConfig.persistConfig.persistDenylist ?? [] + ); + + return JSON.stringify(result); +}; + +const PERSISTED_KEYS = Object.values(SLICE_CONFIGS) + .filter((sliceConfig) => !!sliceConfig.persistConfig) + .map((sliceConfig) => sliceConfig.slice.reducerPath); + +export const createStore = (options?: { persist?: boolean; persistDebounce?: number; onRehydrated?: () => void }) => { + const store = configureStore({ + reducer: rememberedRootReducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + // serializableCheck: false, + // immutableCheck: false, + serializableCheck: import.meta.env.MODE === 'development', + immutableCheck: import.meta.env.MODE === 'development', + }) + .concat(api.middleware) + // .concat(getDebugLoggerMiddleware({ withDiff: true, withNextState: true })) + .prepend(listenerMiddleware.middleware), + enhancers: (getDefaultEnhancers) => { + const enhancers = getDefaultEnhancers(); + if (options?.persist) { + return enhancers.prepend( + rememberEnhancer(reduxRememberDriver, PERSISTED_KEYS, { + persistDebounce: options?.persistDebounce ?? 2000, + serialize, + unserialize, + prefix: '', + errorHandler, + }) + ); + } else { + return enhancers; + } + }, + devTools: { + actionSanitizer, + stateSanitizer, + trace: true, + predicate: (state, action) => { + if (actionsDenylist.includes(action.type)) { + return false; + } + return true; + }, + }, + }); + + // Once-off listener to support waiting for rehydration before rendering the app + startAppListening({ + actionCreator: createAction(REMEMBER_REHYDRATED), + effect: (action, { unsubscribe }) => { + unsubscribe(); + options?.onRehydrated?.(); + }, + }); + + return store; +}; + +export type AppStore = ReturnType; +export type RootState = ReturnType; +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +export type AppThunkDispatch = ThunkDispatch; +export type AppDispatch = ReturnType['dispatch']; +export type AppGetState = ReturnType['getState']; +export type AppStartListening = TypedStartListening; + +export const addAppListener = addListener.withTypes(); + +// To avoid circular dependencies, all listener middleware listeners are added here in the main store setup file. +const startAppListening = listenerMiddleware.startListening as AppStartListening; +addImageUploadedFulfilledListener(startAppListening); + +// Image deleted +addDeleteBoardAndImagesFulfilledListener(startAppListening); + +// User Invoked +addAnyEnqueuedListener(startAppListening); +addBatchEnqueuedListener(startAppListening); + +// Socket.IO +addSocketConnectedEventListener(startAppListening); + +// Gallery bulk download +addBulkDownloadListeners(startAppListening); + +// Boards +addImageAddedToBoardFulfilledListener(startAppListening); +addImageRemovedFromBoardFulfilledListener(startAppListening); +addBoardIdSelectedListener(startAppListening); +addArchivedOrDeletedBoardListener(startAppListening); + +// Node schemas +addGetOpenAPISchemaListener(startAppListening); + +// Models +addModelSelectedListener(startAppListening); + +// app startup +addAppStartedListener(startAppListening); +addModelsLoadedListener(startAppListening); + +// Ad-hoc upscale workflwo +addAdHocPostProcessingRequestedListener(startAppListening); + +// Filters +addPBRFilterListener(startAppListening); + +addSetDefaultSettingsListener(startAppListening); diff --git a/invokeai/frontend/web/src/app/store/storeHooks.ts b/invokeai/frontend/web/src/app/store/storeHooks.ts new file mode 100644 index 00000000000..cd0e41e55d1 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/storeHooks.ts @@ -0,0 +1,8 @@ +import type { AppStore, AppThunkDispatch, RootState } from 'app/store/store'; +import type { TypedUseSelectorHook } from 'react-redux'; +import { useDispatch, useSelector, useStore } from 'react-redux'; + +// Use throughout your app instead of plain `useDispatch` and `useSelector` +export const useAppDispatch = () => useDispatch(); +export const useAppSelector: TypedUseSelectorHook = useSelector; +export const useAppStore = () => useStore.withTypes()(); diff --git a/invokeai/frontend/web/src/app/store/types.ts b/invokeai/frontend/web/src/app/store/types.ts new file mode 100644 index 00000000000..28b28e1889b --- /dev/null +++ b/invokeai/frontend/web/src/app/store/types.ts @@ -0,0 +1,46 @@ +import type { Slice } from '@reduxjs/toolkit'; +import type { UndoableOptions } from 'redux-undo'; +import type { ZodType } from 'zod'; + +type StateFromSlice = T extends Slice ? U : never; + +export type SliceConfig = { + /** + * The redux slice (return of createSlice). + */ + slice: T; + /** + * The zod schema for the slice. + */ + schema: ZodType>; + /** + * A function that returns the initial state of the slice. + */ + getInitialState: () => StateFromSlice; + /** + * The optional persist configuration for this slice. If omitted, the slice will not be persisted. + */ + persistConfig?: { + /** + * Migrate the state to the current version during rehydration. This method should throw an error if the migration + * fails. + * + * @param state The rehydrated state. + * @returns A correctly-shaped state. + */ + migrate: (state: unknown) => StateFromSlice; + /** + * Keys to omit from the persisted state. + */ + persistDenylist?: (keyof StateFromSlice)[]; + }; + /** + * The optional undoable configuration for this slice. If omitted, the slice will not be undoable. + */ + undoableConfig?: { + /** + * The options to be passed into redux-undo. + */ + reduxUndoOptions: UndoableOptions>; + }; +}; diff --git a/invokeai/frontend/web/src/app/store/use-debounced-app-selector.ts b/invokeai/frontend/web/src/app/store/use-debounced-app-selector.ts new file mode 100644 index 00000000000..83fe538f751 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/use-debounced-app-selector.ts @@ -0,0 +1,43 @@ +import type { Selector } from '@reduxjs/toolkit'; +import type { RootState } from 'app/store/store'; +import { useAppStore } from 'app/store/storeHooks'; +import { useEffect, useState } from 'react'; + +/** + * A hook that returns a debounced value from the app state. + * + * @param selector The redux selector + * @param debounceMs The debounce time in milliseconds + * @returns The debounced value + */ +export const useDebouncedAppSelector = (selector: Selector, debounceMs: number = 300) => { + const store = useAppStore(); + const [value, setValue] = useState(() => selector(store.getState())); + + useEffect(() => { + let prevValue = selector(store.getState()); + let timeout: number | null = null; + + const unsubscribe = store.subscribe(() => { + const value = selector(store.getState()); + if (value !== prevValue) { + if (timeout !== null) { + window.clearTimeout(timeout); + } + timeout = window.setTimeout(() => { + setValue(value); + prevValue = value; + }, debounceMs); + } + }); + + return () => { + unsubscribe(); + if (timeout !== null) { + window.clearTimeout(timeout); + } + }; + }, [debounceMs, selector, store]); + + return value; +}; diff --git a/invokeai/frontend/web/src/common/components/ColorPicker/RgbColorPicker.tsx b/invokeai/frontend/web/src/common/components/ColorPicker/RgbColorPicker.tsx new file mode 100644 index 00000000000..1361039b756 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/ColorPicker/RgbColorPicker.tsx @@ -0,0 +1,105 @@ +import type { ChakraProps } from '@invoke-ai/ui-library'; +import { Box, CompositeNumberInput, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { RGB_COLOR_SWATCHES } from 'common/components/ColorPicker/swatches'; +import { rgbColorToString } from 'common/util/colorCodeTransformers'; +import type { CSSProperties } from 'react'; +import { memo, useCallback } from 'react'; +import { RgbColorPicker as ColorfulRgbColorPicker } from 'react-colorful'; +import type { RgbColor } from 'react-colorful/dist/types'; +import { useTranslation } from 'react-i18next'; + +type Props = { + color: RgbColor; + onChange: (color: RgbColor) => void; + withNumberInput?: boolean; + withSwatches?: boolean; +}; +const colorPickerPointerStyles: NonNullable = { + width: 6, + height: 6, + borderColor: 'base.100', +}; + +const sx: ChakraProps['sx'] = { + '.react-colorful__hue-pointer': colorPickerPointerStyles, + '.react-colorful__saturation-pointer': colorPickerPointerStyles, + '.react-colorful__alpha-pointer': colorPickerPointerStyles, + gap: 4, + flexDir: 'column', +}; + +const colorPickerStyles: CSSProperties = { width: '100%' }; + +const numberInputWidth: ChakraProps['w'] = '3.5rem'; + +const RgbColorPicker = (props: Props) => { + const { color, onChange, withNumberInput = false, withSwatches = false } = props; + const { t } = useTranslation(); + const handleChangeR = useCallback((r: number) => onChange({ ...color, r }), [color, onChange]); + const handleChangeG = useCallback((g: number) => onChange({ ...color, g }), [color, onChange]); + const handleChangeB = useCallback((b: number) => onChange({ ...color, b }), [color, onChange]); + return ( + + + {withNumberInput && ( + + + {t('common.red')[0]} + + + + {t('common.green')[0]} + + + + {t('common.blue')[0]} + + + + )} + {withSwatches && ( + + {RGB_COLOR_SWATCHES.map((color, i) => ( + + ))} + + )} + + ); +}; + +export default memo(RgbColorPicker); + +const ColorSwatch = ({ color, onChange }: { color: RgbColor; onChange: (color: RgbColor) => void }) => { + const onClick = useCallback(() => { + onChange(color); + }, [color, onChange]); + return ; +}; diff --git a/invokeai/frontend/web/src/common/components/ColorPicker/RgbaColorPicker.tsx b/invokeai/frontend/web/src/common/components/ColorPicker/RgbaColorPicker.tsx new file mode 100644 index 00000000000..38fcb2696ef --- /dev/null +++ b/invokeai/frontend/web/src/common/components/ColorPicker/RgbaColorPicker.tsx @@ -0,0 +1,186 @@ +import type { ChakraProps } from '@invoke-ai/ui-library'; +import { Box, Button, CompositeNumberInput, Flex, FormControl, FormLabel, Input } from '@invoke-ai/ui-library'; +import { RGBA_COLOR_SWATCHES } from 'common/components/ColorPicker/swatches'; +import { hexToRGBA, rgbaColorToString, rgbaToHex } from 'common/util/colorCodeTransformers'; +import type { ChangeEvent, CSSProperties } from 'react'; +import { memo, useCallback, useEffect, useState } from 'react'; +import { RgbaColorPicker as ColorfulRgbaColorPicker } from 'react-colorful'; +import type { RgbaColor } from 'react-colorful/dist/types'; +import { useTranslation } from 'react-i18next'; + +type Props = { + color: RgbaColor; + onChange: (color: RgbaColor) => void; + withNumberInput?: boolean; + withSwatches?: boolean; +}; + +const colorPickerPointerStyles: NonNullable = { + width: 6, + height: 6, + borderColor: 'base.100', +}; + +const sx: ChakraProps['sx'] = { + '.react-colorful__hue-pointer': colorPickerPointerStyles, + '.react-colorful__saturation-pointer': colorPickerPointerStyles, + '.react-colorful__alpha-pointer': colorPickerPointerStyles, + gap: 4, + flexDir: 'column', +}; + +const colorPickerStyles: CSSProperties = { width: '100%' }; + +const numberInputWidth: ChakraProps['w'] = '3.5rem'; + +const RgbaColorPicker = (props: Props) => { + const { color, onChange, withNumberInput = false, withSwatches = false } = props; + const { t } = useTranslation(); + const handleChangeR = useCallback((r: number) => onChange({ ...color, r }), [color, onChange]); + const handleChangeG = useCallback((g: number) => onChange({ ...color, g }), [color, onChange]); + const handleChangeB = useCallback((b: number) => onChange({ ...color, b }), [color, onChange]); + const handleChangeA = useCallback((a: number) => onChange({ ...color, a }), [color, onChange]); + const [mode, setMode] = useState<'rgb' | 'hex'>('rgb'); + const [hex, setHex] = useState(rgbaToHex(color, true)); + useEffect(() => { + setHex(rgbaToHex(color, true)); + }, [color]); + const onToggleMode = useCallback(() => setMode((m) => (m === 'rgb' ? 'hex' : 'rgb')), []); + const onChangeHex = useCallback( + (e: ChangeEvent) => { + let value = e.target.value.trim(); + if (!value.startsWith('#')) { + value = `#${value}`; + } + const cleaned = value.replace(/[^#0-9a-fA-F]/g, '').slice(0, 9); + setHex(cleaned); + const hexBody = cleaned.replace('#', ''); + if (hexBody.length === 6 || hexBody.length === 8) { + const a = hexBody.length === 8 ? parseInt(hexBody.slice(6, 8), 16) / 255 : color.a; + const next = hexToRGBA(hexBody.slice(0, 6).padEnd(6, '0'), a); + onChange(next); + } + }, + [color.a, onChange] + ); + const onChangeAlpha = useCallback( + (a: number) => { + const next = { ...color, a: Math.max(0, Math.min(1, a)) }; + onChange(next); + setHex(rgbaToHex(next, true)); + }, + [color, onChange] + ); + return ( + + + {withNumberInput && + (mode === 'rgb' ? ( + + + + {t('common.red')[0]} + + + + {t('common.green')[0]} + + + + {t('common.blue')[0]} + + + + {t('common.alpha')[0]} + + + + ) : ( + + + + {t('common.hex')} + + + + {t('common.alpha')[0]} + + + + ))} + {withSwatches && ( + + {RGBA_COLOR_SWATCHES.map((color, i) => ( + + ))} + + )} + + ); +}; + +export default memo(RgbaColorPicker); + +const ColorSwatch = ({ color, onChange }: { color: RgbaColor; onChange: (color: RgbaColor) => void }) => { + const onClick = useCallback(() => { + onChange(color); + }, [color, onChange]); + return ; +}; diff --git a/invokeai/frontend/web/src/common/components/ColorPicker/swatches.ts b/invokeai/frontend/web/src/common/components/ColorPicker/swatches.ts new file mode 100644 index 00000000000..0bbbcfe3da3 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/ColorPicker/swatches.ts @@ -0,0 +1,16 @@ +const SWATCHES = [ + { r: 0, g: 0, b: 0, a: 1 }, // black + { r: 255, g: 255, b: 255, a: 1 }, // white + { r: 255, g: 90, b: 94, a: 1 }, // red + { r: 255, g: 146, b: 75, a: 1 }, // orange + { r: 255, g: 202, b: 59, a: 1 }, // yellow + { r: 197, g: 202, b: 48, a: 1 }, // lime + { r: 138, g: 201, b: 38, a: 1 }, // green + { r: 83, g: 165, b: 117, a: 1 }, // teal + { r: 23, g: 130, b: 196, a: 1 }, // blue + { r: 66, g: 103, b: 172, a: 1 }, // indigo + { r: 107, g: 76, b: 147, a: 1 }, // purple +]; + +export const RGBA_COLOR_SWATCHES = SWATCHES; +export const RGB_COLOR_SWATCHES = SWATCHES.map(({ r, g, b }) => ({ r, g, b })); diff --git a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx new file mode 100644 index 00000000000..756ffec0d91 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx @@ -0,0 +1,91 @@ +import type { ChakraProps, FlexProps } from '@invoke-ai/ui-library'; +import { Flex, Icon, Skeleton, Spinner, Text } from '@invoke-ai/ui-library'; +import type { ElementType } from 'react'; +import { memo, useMemo } from 'react'; +import { PiImageBold } from 'react-icons/pi'; +import type { ImageDTO } from 'services/api/types'; + +type Props = { image: ImageDTO | undefined }; + +const IAILoadingImageFallback = memo((props: Props) => { + if (props.image) { + return ( + + ); + } + + return ( + + + + ); +}); +IAILoadingImageFallback.displayName = 'IAILoadingImageFallback'; + +type IAINoImageFallbackProps = FlexProps & { + label?: string; + icon?: ElementType | null; + boxSize?: ChakraProps['boxSize']; +}; + +export const IAINoContentFallback = memo((props: IAINoImageFallbackProps) => { + const { icon = PiImageBold, boxSize = 16, ...rest } = props; + + return ( + + {icon && } + {props.label && {props.label}} + + ); +}); +IAINoContentFallback.displayName = 'IAINoContentFallback'; + +type IAINoImageFallbackWithSpinnerProps = FlexProps & { + label?: string; +}; + +export const IAINoContentFallbackWithSpinner = memo((props: IAINoImageFallbackWithSpinnerProps) => { + const { sx, ...rest } = props; + const styles = useMemo( + () => ({ + w: 'full', + h: 'full', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 'base', + flexDir: 'column', + gap: 2, + userSelect: 'none', + opacity: 0.7, + color: 'base.500', + ...sx, + }), + [sx] + ); + + return ( + + + {props.label && {props.label}} + + ); +}); +IAINoContentFallbackWithSpinner.displayName = 'IAINoContentFallbackWithSpinner'; diff --git a/invokeai/frontend/web/src/common/components/IconMenuItem.tsx b/invokeai/frontend/web/src/common/components/IconMenuItem.tsx new file mode 100644 index 00000000000..6b58d5a6112 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IconMenuItem.tsx @@ -0,0 +1,34 @@ +import type { MenuItemProps } from '@invoke-ai/ui-library'; +import { Flex, MenuItem, Tooltip } from '@invoke-ai/ui-library'; +import type { ReactNode } from 'react'; + +type Props = MenuItemProps & { + tooltip?: ReactNode; + icon: ReactNode; +}; + +export const IconMenuItem = ({ tooltip, icon, ...props }: Props) => { + return ( + + + {icon} + + + ); +}; + +export const IconMenuItemGroup = ({ children }: { children: ReactNode }) => { + return ( + + {children} + + ); +}; diff --git a/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx b/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx new file mode 100644 index 00000000000..276bd3b4505 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx @@ -0,0 +1,153 @@ +import { + Button, + Divider, + Flex, + Heading, + Image, + Popover, + PopoverBody, + PopoverCloseButton, + PopoverContent, + PopoverTrigger, + Portal, + Spacer, + Text, +} from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { merge, omit } from 'es-toolkit/compat'; +import { selectSystemSlice, setShouldEnableInformationalPopovers } from 'features/system/store/systemSlice'; +import { toast } from 'features/toast/toast'; +import type { ReactElement } from 'react'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiArrowSquareOutBold } from 'react-icons/pi'; + +import type { Feature, PopoverData } from './constants'; +import { OPEN_DELAY, POPOVER_DATA, POPPER_MODIFIERS } from './constants'; + +type Props = { + feature: Feature; + inPortal?: boolean; + hideDisable?: boolean; + children: ReactElement; +}; + +const selectShouldEnableInformationalPopovers = createSelector( + selectSystemSlice, + (system) => system.shouldEnableInformationalPopovers +); + +export const InformationalPopover = memo( + ({ feature, children, inPortal = true, hideDisable = false, ...rest }: Props) => { + const shouldEnableInformationalPopovers = useAppSelector(selectShouldEnableInformationalPopovers); + + const data = useMemo(() => POPOVER_DATA[feature], [feature]); + + const popoverProps = useMemo(() => merge(omit(data, ['image', 'href', 'buttonLabel']), rest), [data, rest]); + + if (!hideDisable && !shouldEnableInformationalPopovers) { + return children; + } + + return ( + + {children} + {inPortal ? ( + + + + ) : ( + + )} + + ); + } +); + +InformationalPopover.displayName = 'InformationalPopover'; + +type ContentProps = { + data?: PopoverData; + feature: Feature; + hideDisable: boolean; +}; + +const Content = ({ data, feature, hideDisable }: ContentProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const heading = useMemo(() => t(`popovers.${feature}.heading`), [feature, t]); + + const paragraphs = useMemo( + () => + t(`popovers.${feature}.paragraphs`, { + returnObjects: true, + }) ?? [], + [feature, t] + ); + + const onClickLearnMore = useCallback(() => { + if (!data?.href) { + return; + } + window.open(data.href); + }, [data?.href]); + + const onClickDontShowMeThese = useCallback(() => { + dispatch(setShouldEnableInformationalPopovers(false)); + toast({ + title: t('settings.informationalPopoversDisabled'), + description: t('settings.informationalPopoversDisabledDesc'), + status: 'info', + }); + }, [dispatch, t]); + + return ( + + + + + {heading && ( + <> + {heading} + + + )} + {data?.image && ( + <> + Optional Image + + + )} + {paragraphs.map((p) => ( + {p} + ))} + + + + {!hideDisable && ( + + )} + + {data?.href && ( + + )} + + + + + ); +}; diff --git a/invokeai/frontend/web/src/common/components/InformationalPopover/constants.ts b/invokeai/frontend/web/src/common/components/InformationalPopover/constants.ts new file mode 100644 index 00000000000..e9d855648ad --- /dev/null +++ b/invokeai/frontend/web/src/common/components/InformationalPopover/constants.ts @@ -0,0 +1,241 @@ +import type { PopoverProps } from '@invoke-ai/ui-library'; +import denoisingStrength from 'public/assets/images/denoising-strength.png'; + +export type Feature = + | 'clipSkip' + | 'fluxDypePreset' + | 'fluxDypeScale' + | 'fluxDypeExponent' + | 'hrf' + | 'paramNegativeConditioning' + | 'paramPositiveConditioning' + | 'paramScheduler' + | 'seedVarianceEnhancer' + | 'seedVarianceStrength' + | 'seedVarianceRandomizePercent' + | 'compositingMaskBlur' + | 'compositingBlurMethod' + | 'compositingCoherencePass' + | 'compositingCoherenceMode' + | 'compositingCoherenceEdgeSize' + | 'compositingCoherenceMinDenoise' + | 'compositingMaskAdjustments' + | 'controlNet' + | 'controlNetBeginEnd' + | 'controlNetControlMode' + | 'controlNetProcessor' + | 'controlNetResizeMode' + | 'controlNetWeight' + | 'dynamicPrompts' + | 'dynamicPromptsMaxPrompts' + | 'dynamicPromptsSeedBehaviour' + | 'globalReferenceImage' + | 'imageFit' + | 'infillMethod' + | 'inpainting' + | 'ipAdapterMethod' + | 'lora' + | 'loraWeight' + | 'noiseUseCPU' + | 'paramAspect' + | 'paramCFGScale' + | 'paramGuidance' + | 'paramCFGRescaleMultiplier' + | 'paramDenoisingStrength' + | 'paramHeight' + | 'paramHrf' + | 'paramIterations' + | 'paramModel' + | 'paramRatio' + | 'paramSeed' + | 'paramSteps' + | 'paramUpscaleMethod' + | 'paramVAE' + | 'paramVAEPrecision' + | 'paramWidth' + | 'patchmatchDownScaleSize' + | 'rasterLayer' + | 'refinerModel' + | 'refinerNegativeAestheticScore' + | 'refinerPositiveAestheticScore' + | 'refinerScheduler' + | 'refinerStart' + | 'refinerSteps' + | 'refinerCfgScale' + | 'regionalGuidance' + | 'regionalGuidanceAndReferenceImage' + | 'regionalReferenceImage' + | 'scaleBeforeProcessing' + | 'seamlessTilingXAxis' + | 'seamlessTilingYAxis' + | 'colorCompensation' + | 'upscaleModel' + | 'scale' + | 'creativity' + | 'structure' + | 'tileSize' + | 'tileOverlap' + | 'optimizedDenoising' + | 'fluxDevLicense' + | 'cpuOnly' + | 'fp8Storage'; + +export type PopoverData = PopoverProps & { + image?: string; + href?: string; + buttonLabel?: string; +}; + +export const POPOVER_DATA: { [key in Feature]?: PopoverData } = { + paramNegativeConditioning: { + placement: 'right', + }, + clipSkip: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings', + }, + fluxDypePreset: { + placement: 'right', + }, + fluxDypeScale: { + placement: 'right', + }, + fluxDypeExponent: { + placement: 'right', + }, + inpainting: { + href: 'https://support.invoke.ai/support/solutions/articles/151000096702-inpainting-outpainting-and-bounding-box', + }, + rasterLayer: { + href: 'https://support.invoke.ai/support/solutions/articles/151000094998-raster-layers-and-initial-images', + }, + regionalGuidance: { + href: 'https://support.invoke.ai/support/solutions/articles/151000165024-regional-guidance-layers', + }, + regionalGuidanceAndReferenceImage: { + href: 'https://support.invoke.ai/support/solutions/articles/151000165024-regional-guidance-layers', + }, + globalReferenceImage: { + href: 'https://support.invoke.ai/support/solutions/articles/151000159340-global-and-regional-reference-images-ip-adapters-', + }, + regionalReferenceImage: { + href: 'https://support.invoke.ai/support/solutions/articles/151000159340-global-and-regional-reference-images-ip-adapters-', + }, + controlNet: { + href: 'https://support.invoke.ai/support/solutions/articles/151000105880', + }, + controlNetBeginEnd: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178148', + }, + controlNetWeight: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178148', + }, + lora: { + href: 'https://support.invoke.ai/support/solutions/articles/151000159072', + }, + loraWeight: { + href: 'https://support.invoke.ai/support/solutions/articles/151000159072-concepts-low-rank-adaptations-loras-', + }, + compositingMaskBlur: { + href: 'https://support.invoke.ai/support/solutions/articles/151000158838-compositing-settings', + }, + compositingBlurMethod: { + href: 'https://support.invoke.ai/support/solutions/articles/151000158838-compositing-settings', + }, + compositingCoherenceMode: { + href: 'https://support.invoke.ai/support/solutions/articles/151000158838-compositing-settings', + }, + infillMethod: { + href: 'https://support.invoke.ai/support/solutions/articles/151000158838-compositing-settings', + }, + scaleBeforeProcessing: { + href: 'https://support.invoke.ai/support/solutions/articles/151000179777-scale-before-processing', + }, + paramCFGScale: { + href: 'https://www.youtube.com/watch?v=1OeHEJrsTpI', + }, + paramCFGRescaleMultiplier: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings', + }, + paramDenoisingStrength: { + href: 'https://support.invoke.ai/support/solutions/articles/151000094998-image-to-image', + image: denoisingStrength, + }, + paramHrf: { + href: 'https://support.invoke.ai/support/solutions/articles/151000096700-how-can-i-get-larger-images-what-does-upscaling-do-', + }, + paramIterations: { + href: 'https://support.invoke.ai/support/solutions/articles/151000159073', + }, + paramPositiveConditioning: { + href: 'https://support.invoke.ai/support/solutions/articles/151000096606-tips-on-crafting-prompts', + placement: 'right', + }, + paramScheduler: { + placement: 'right', + href: 'https://www.youtube.com/watch?v=1OeHEJrsTpI', + }, + paramSeed: { + href: 'https://support.invoke.ai/support/solutions/articles/151000096684-what-is-a-seed-how-do-i-use-it-to-recreate-the-same-image-', + }, + paramModel: { + placement: 'right', + href: 'https://support.invoke.ai/support/solutions/articles/151000096601-what-is-a-model-which-should-i-use-', + }, + paramRatio: { + gutter: 16, + }, + controlNetControlMode: { + placement: 'right', + href: 'https://support.invoke.ai/support/solutions/articles/151000178148', + }, + controlNetProcessor: { + placement: 'right', + href: 'https://support.invoke.ai/support/solutions/articles/151000105880-using-controlnet', + }, + controlNetResizeMode: { + placement: 'right', + href: 'https://support.invoke.ai/support/solutions/articles/151000178148', + }, + paramVAE: { + placement: 'right', + href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings', + }, + paramVAEPrecision: { + placement: 'right', + href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings', + }, + paramUpscaleMethod: { + href: 'https://support.invoke.ai/support/solutions/articles/151000096700-how-can-i-get-larger-images-what-does-upscaling-do-', + }, + refinerModel: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner', + }, + refinerNegativeAestheticScore: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner', + }, + refinerPositiveAestheticScore: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner', + }, + refinerScheduler: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner', + }, + refinerStart: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner', + }, + refinerSteps: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner', + }, + refinerCfgScale: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner', + }, + seamlessTilingXAxis: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings', + }, + seamlessTilingYAxis: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings', + }, +} as const; + +export const OPEN_DELAY = 1000; // in milliseconds + +export const POPPER_MODIFIERS: PopoverProps['modifiers'] = [{ name: 'preventOverflow', options: { padding: 10 } }]; diff --git a/invokeai/frontend/web/src/common/components/InvokeLogoIcon.tsx b/invokeai/frontend/web/src/common/components/InvokeLogoIcon.tsx new file mode 100644 index 00000000000..e744e1a5c85 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/InvokeLogoIcon.tsx @@ -0,0 +1,13 @@ +import type { IconProps } from '@invoke-ai/ui-library'; +import { Icon } from '@invoke-ai/ui-library'; +import { memo } from 'react'; + +export const InvokeLogoIcon = memo((props: IconProps) => { + return ( + + + + ); +}); + +InvokeLogoIcon.displayName = 'InvokeLogoIcon'; diff --git a/invokeai/frontend/web/src/common/components/Loading/Loading.tsx b/invokeai/frontend/web/src/common/components/Loading/Loading.tsx new file mode 100644 index 00000000000..63798f46fa9 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/Loading/Loading.tsx @@ -0,0 +1,32 @@ +import { Flex, Image, Spinner } from '@invoke-ai/ui-library'; +import InvokeLogoWhite from 'public/assets/images/invoke-symbol-wht-lrg.svg'; +import { memo } from 'react'; + +// This component loads before the theme so we cannot use theme tokens here + +const Loading = () => { + return ( + + + + + ); +}; + +export default memo(Loading); diff --git a/invokeai/frontend/web/src/common/components/OverlayScrollbars/ScrollableContent.tsx b/invokeai/frontend/web/src/common/components/OverlayScrollbars/ScrollableContent.tsx new file mode 100644 index 00000000000..5da75b10c69 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/OverlayScrollbars/ScrollableContent.tsx @@ -0,0 +1,58 @@ +import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; +import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element'; +import { autoScrollForExternal } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/external'; +import type { ChakraProps } from '@invoke-ai/ui-library'; +import { Box, Flex } from '@invoke-ai/ui-library'; +import { getOverlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; +import type { OverlayScrollbarsComponentRef } from 'overlayscrollbars-react'; +import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; +import type { CSSProperties, PropsWithChildren } from 'react'; +import { memo, useEffect, useMemo, useState } from 'react'; + +type Props = PropsWithChildren & { + maxHeight?: ChakraProps['maxHeight']; + maxWidth?: ChakraProps['maxWidth']; + overflowX?: 'hidden' | 'scroll'; + overflowY?: 'hidden' | 'scroll'; +}; + +const styles: CSSProperties = { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }; + +const ScrollableContent = ({ children, maxHeight, maxWidth, overflowX = 'hidden', overflowY = 'scroll' }: Props) => { + const overlayscrollbarsOptions = useMemo( + () => getOverlayScrollbarsParams({ overflowX, overflowY }).options, + [overflowX, overflowY] + ); + const [os, osRef] = useState(null); + useEffect(() => { + const osInstance = os?.osInstance(); + + if (!osInstance) { + return; + } + + const element = osInstance.elements().viewport; + + // `pragmatic-drag-and-drop-auto-scroll` requires the element to have `overflow-y: scroll` or `overflow-y: auto` + // else it logs an ugly warning. In our case, using a custom scrollbar library, it will be 'hidden' by default. + // To prevent the erroneous warning, we temporarily set the overflow-y to 'scroll' and then revert it back. + const overflowY = element.style.overflowY; // starts 'hidden' + element.style.setProperty('overflow-y', 'scroll', 'important'); + const cleanup = combine(autoScrollForElements({ element }), autoScrollForExternal({ element })); + element.style.setProperty('overflow-y', overflowY); + + return cleanup; + }, [os]); + + return ( + + + + {children} + + + + ); +}; + +export default memo(ScrollableContent); diff --git a/invokeai/frontend/web/src/common/components/OverlayScrollbars/constants.ts b/invokeai/frontend/web/src/common/components/OverlayScrollbars/constants.ts new file mode 100644 index 00000000000..ea3633900dc --- /dev/null +++ b/invokeai/frontend/web/src/common/components/OverlayScrollbars/constants.ts @@ -0,0 +1,45 @@ +import { deepClone } from 'common/util/deepClone'; +import { merge } from 'es-toolkit/compat'; +import { ClickScrollPlugin, OverlayScrollbars } from 'overlayscrollbars'; +import type { UseOverlayScrollbarsParams } from 'overlayscrollbars-react'; +import type { CSSProperties } from 'react'; + +OverlayScrollbars.plugin(ClickScrollPlugin); + +export const overlayScrollbarsParams: UseOverlayScrollbarsParams = { + defer: true, + options: { + scrollbars: { + visibility: 'auto', + autoHide: 'scroll', + autoHideDelay: 1300, + theme: 'os-theme-dark', + clickScroll: true, + }, + overflow: { x: 'hidden' }, + }, +}; + +export const getOverlayScrollbarsParams = ({ + overflowX = 'hidden', + overflowY = 'scroll', + visibility = 'auto', +}: { + overflowX?: 'hidden' | 'scroll'; + overflowY?: 'hidden' | 'scroll'; + visibility?: 'auto' | 'hidden' | 'visible'; +}) => { + const params = deepClone(overlayScrollbarsParams); + merge(params, { + options: { + overflow: { y: overflowY, x: overflowX }, + scrollbars: { visibility, autoHide: visibility === 'visible' ? 'never' : 'scroll' }, + }, + }); + return params; +}; + +export const overlayScrollbarsStyles: CSSProperties = { + height: '100%', + width: '100%', +}; diff --git a/invokeai/frontend/web/src/common/components/OverlayScrollbars/overlayscrollbars.css b/invokeai/frontend/web/src/common/components/OverlayScrollbars/overlayscrollbars.css new file mode 100644 index 00000000000..96f8a67ccc9 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/OverlayScrollbars/overlayscrollbars.css @@ -0,0 +1,56 @@ +.os-scrollbar { + /* The size of the scrollbar */ + --os-size: 8px; + /* The axis-perpedicular padding of the scrollbar (horizontal: padding-y, vertical: padding-x) */ + /* --os-padding-perpendicular: 0; */ + /* The axis padding of the scrollbar (horizontal: padding-x, vertical: padding-y) */ + /* --os-padding-axis: 0; */ + /* The border radius of the scrollbar track */ + /* --os-track-border-radius: 0; */ + /* The background of the scrollbar track */ + --os-track-bg: rgba(0, 0, 0, 0.5); + /* The :hover background of the scrollbar track */ + --os-track-bg-hover: rgba(0, 0, 0, 0.5); + /* The :active background of the scrollbar track */ + --os-track-bg-active: rgba(0, 0, 0, 0.6); + /* The border of the scrollbar track */ + /* --os-track-border: none; */ + /* The :hover background of the scrollbar track */ + /* --os-track-border-hover: none; */ + /* The :active background of the scrollbar track */ + /* --os-track-border-active: none; */ + /* The border radius of the scrollbar handle */ + /* --os-handle-border-radius: 2px; */ + /* The background of the scrollbar handle */ + --os-handle-bg: var(--invoke-colors-base-400); + /* The :hover background of the scrollbar handle */ + --os-handle-bg-hover: var(--invoke-colors-base-300); + /* The :active background of the scrollbar handle */ + --os-handle-bg-active: var(--invoke-colors-base-250); + /* The border of the scrollbar handle */ + /* --os-handle-border: none; */ + /* The :hover border of the scrollbar handle */ + /* --os-handle-border-hover: none; */ + /* The :active border of the scrollbar handle */ + /* --os-handle-border-active: none; */ + /* The min size of the scrollbar handle */ + --os-handle-min-size: 32px; + /* The max size of the scrollbar handle */ + /* --os-handle-max-size: none; */ + /* The axis-perpedicular size of the scrollbar handle (horizontal: height, vertical: width) */ + /* --os-handle-perpendicular-size: 100%; */ + /* The :hover axis-perpedicular size of the scrollbar handle (horizontal: height, vertical: width) */ + --os-handle-perpendicular-size-hover: 100%; + /* The :active axis-perpedicular size of the scrollbar handle (horizontal: height, vertical: width) */ + /* --os-handle-perpendicular-size-active: 100%; */ + /* Increases the interactive area of the scrollbar handle. */ + --os-handle-interactive-area-offset: -1px; +} + +.os-scrollbar-handle { + /* cursor: grab; */ +} + +.os-scrollbar-handle:active { + /* cursor: grabbing; */ +} diff --git a/invokeai/frontend/web/src/common/components/Picker/Picker.tsx b/invokeai/frontend/web/src/common/components/Picker/Picker.tsx new file mode 100644 index 00000000000..b70e44dd649 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/Picker/Picker.tsx @@ -0,0 +1,1126 @@ +import type { BoxProps, SystemStyleObject } from '@invoke-ai/ui-library'; +import { + Badge, + Divider, + Flex, + IconButton, + Input, + InputGroup, + InputRightElement, + Spacer, + Text, +} from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; +import { typedMemo } from 'common/util/typedMemo'; +import { NO_DRAG_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants'; +import { selectPickerCompactViewStates } from 'features/ui/store/uiSelectors'; +import { pickerCompactViewStateChanged } from 'features/ui/store/uiSlice'; +import type { AnyStore, ReadableAtom, Task, WritableAtom } from 'nanostores'; +import { atom, computed } from 'nanostores'; +import type { StoreValues } from 'nanostores/computed'; +import type { ChangeEvent, MouseEventHandler, PropsWithChildren, RefObject } from 'react'; +import React, { + createContext, + useCallback, + useContext, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import { + PiArrowCounterClockwiseBold, + PiArrowsInLineVerticalBold, + PiArrowsOutLineVerticalBold, + PiXBold, +} from 'react-icons/pi'; +import { assert } from 'tsafe'; +import { useDebounce } from 'use-debounce'; + +const NO_WHEEL_NO_DRAG_CLASS = `${NO_WHEEL_CLASS} ${NO_DRAG_CLASS}`; + +const uniqueGroupKey = Symbol('uniqueGroupKey'); + +export type Group = { + /** + * The unique id of the group. + */ + id: string; + /** + * The options in the group. + */ + options: T[]; + /** + * The color of the group. Used to style the group toggle button and vertical group line. + * + * It can be a CSS color string or theme color token. + */ + color?: string; + /** + * The name of the group. + */ + name?: string; + /** + * The short name of the group. Used to display for the group toggle button. + */ + shortName?: string; + /** + * The description of the group. Used to display in the group toggle button. + */ + description?: string; + /** + * A function that returns a "count" string for the group. It will be called with the number of matching options in + * the group. + */ + getOptionCountString?: (count: number) => string; + /** + * A unique key used for type-checking the group. Use the `buildGroup` function to create a group, which will set this key. + */ + [uniqueGroupKey]: true; +}; + +type OptionOrGroup = T | Group; + +export const buildGroup = (group: Omit, typeof uniqueGroupKey>): Group => ({ + ...group, + [uniqueGroupKey]: true, +}); + +export const isGroup = (optionOrGroup: OptionOrGroup): optionOrGroup is Group => { + return uniqueGroupKey in optionOrGroup && optionOrGroup[uniqueGroupKey] === true; +}; + +const DefaultOptionComponent = typedMemo(({ option }: { option: T }) => { + const { getOptionId } = usePickerContext(); + return {getOptionId(option)}; +}); +DefaultOptionComponent.displayName = 'DefaultOptionComponent'; + +const DefaultGroupComponent = typedMemo( + ({ group, children }: PropsWithChildren<{ group: Group }>) => { + return ( + + {group.id} + + {children} + + + ); + } +); +DefaultGroupComponent.displayName = 'DefaultGroupComponent'; + +const NoOptionsFallbackWrapper = typedMemo(({ children }: PropsWithChildren) => { + const { t } = useTranslation(); + return ( + + {typeof children === 'string' ? ( + {children} + ) : ( + (children ?? {t('common.noOptions')}) + )} + + ); +}); +NoOptionsFallbackWrapper.displayName = 'NoOptionsFallbackWrapper'; + +const NoMatchesFallbackWrapper = typedMemo(({ children }: PropsWithChildren) => { + const { t } = useTranslation(); + return ( + + {typeof children === 'string' ? ( + {children} + ) : ( + (children ?? {t('common.noMatches')}) + )} + + ); +}); +NoMatchesFallbackWrapper.displayName = 'NoMatchesFallbackWrapper'; + +type PickerProps = { + /** + * Unique identifier for this picker instance. Used to persist compact view state. + */ + pickerId?: string; + /** + * The options to display in the picker. This can be a flat array of options or an array of groups. + */ + optionsOrGroups: OptionOrGroup[]; + /** + * A function that returns the id of an option. + */ + getOptionId: (option: T) => string; + /** + * A function that returns true if the option matches the search term. + */ + isMatch: (option: T, searchTerm: string) => boolean; + /** + * A function that returns true if the option is disabled. + */ + getIsOptionDisabled?: (option: T) => boolean; + /** + * The currently selected item. + */ + selectedOption?: T; + /** + * A function that is called when an option is selected. + */ + onSelect?: (option: T) => void; + /** + * A function that is called when the picker is closed. + */ + onClose?: () => void; + /** + * A placeholder for the search input. + */ + searchPlaceholder?: string; + /** + * A ref to an imperative handle that can be used to control the picker. + */ + handleRef?: React.Ref>; + /** + * A custom option component. If not provided, a default option component will be used. + */ + OptionComponent?: React.ComponentType<{ option: T } & BoxProps>; + /** + * A component to render next to the search bar. + */ + NextToSearchBar?: React.ReactNode; + /** + * A fallback component to display when there are no options. If a string is provided, it will be formatted + * as a text element with appropriate styling. If a React node is provided, it will be rendered as is. + */ + noOptionsFallback?: React.ReactNode; + /** + * A fallback component to display when there are no matches. If a string is provided, it will be formatted + * as a text element with appropriate styling. If a React node is provided, it will be rendered as is. + */ + noMatchesFallback?: React.ReactNode; + /** + * Whether the picker should be searchable. If true, renders a search input. + */ + searchable?: boolean; + /** + * Initial state for group toggles. If provided, groups will start with these states instead of all being disabled. + */ + initialGroupStates?: GroupStatusMap; +}; + +const buildSelectIsCompactView = (pickerId?: string) => + createSelector([selectPickerCompactViewStates], (compactViewStates) => { + if (!pickerId) { + return true; + } + return compactViewStates[pickerId] ?? true; + }); + +export type PickerContextState = { + $optionsOrGroups: WritableAtom[]>; + $groupStatusMap: WritableAtom; + isCompactView: boolean; + $activeOptionId: WritableAtom; + $filteredOptions: WritableAtom[]>; + $flattenedFilteredOptions: ReadableAtom; + $totalOptionCount: ReadableAtom; + $hasOptions: ReadableAtom; + $filteredOptionsCount: ReadableAtom; + $hasFilteredOptions: ReadableAtom; + $areAllGroupsDisabled: ReadableAtom; + $selectedItem: WritableAtom; + $selectedItemId: ReadableAtom; + $searchTerm: WritableAtom; + searchPlaceholder?: string; + toggleGroup: (id: string) => void; + getOptionId: (option: T) => string; + isMatch: (option: T, searchTerm: string) => boolean; + getIsOptionDisabled?: (option: T) => boolean; + onSelectById: (id: string) => void; + onClose?: () => void; + rootRef: RefObject; + inputRef: RefObject; + noOptionsFallback?: React.ReactNode; + noMatchesFallback?: React.ReactNode; + OptionComponent: React.ComponentType<{ option: T } & BoxProps>; + NextToSearchBar?: React.ReactNode; + searchable?: boolean; + pickerId?: string; +}; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const PickerContext = createContext | null>(null); +export const usePickerContext = (): PickerContextState => { + const context = useContext(PickerContext); + assert(context !== null, 'usePickerContext must be used within a PickerProvider'); + return context; +}; + +export const getRegex = (searchTerm: string) => { + const terms = searchTerm + .trim() + .replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '') + .split(' ') + .filter((term) => term.length > 0); + + if (terms.length === 0) { + return new RegExp('', 'gi'); + } + + // Create positive lookaheads for each term - matches in any order + const pattern = terms.map((term) => `(?=.*${term})`).join(''); + return new RegExp(`${pattern}.+`, 'i'); +}; + +const getFirstOption = (options: OptionOrGroup[]): T | undefined => { + const firstOptionOrGroup = options[0]; + if (!firstOptionOrGroup) { + return; + } + if (isGroup(firstOptionOrGroup)) { + return firstOptionOrGroup.options[0]; + } else { + return firstOptionOrGroup; + } +}; + +const getFirstOptionId = ( + options: OptionOrGroup[], + getOptionId: (item: T) => string +): string | undefined => { + const firstOptionOrGroup = getFirstOption(options); + if (firstOptionOrGroup) { + return getOptionId(firstOptionOrGroup); + } else { + return undefined; + } +}; + +const findOption = ( + options: OptionOrGroup[], + id: string, + getOptionId: (item: T) => string +): T | undefined => { + for (const optionOrGroup of options) { + if (isGroup(optionOrGroup)) { + const option = optionOrGroup.options.find((opt) => getOptionId(opt) === id); + if (option) { + return option; + } + } else { + if (getOptionId(optionOrGroup) === id) { + return optionOrGroup; + } + } + } +}; + +const flattenOptions = (options: OptionOrGroup[]): T[] => { + const flattened: T[] = []; + for (const optionOrGroup of options) { + if (isGroup(optionOrGroup)) { + flattened.push(...optionOrGroup.options); + } else { + flattened.push(optionOrGroup); + } + } + return flattened; +}; + +export type GroupStatusMap = Record; + +const useTogglableGroups = (options: OptionOrGroup[], initialGroupStates?: GroupStatusMap) => { + const groupsWithOptions = useMemo(() => { + const ids: string[] = []; + for (const optionOrGroup of options) { + if (isGroup(optionOrGroup) && !ids.includes(optionOrGroup.id)) { + ids.push(optionOrGroup.id); + } + } + return ids; + }, [options]); + + const [$groupStatusMap] = useState(atom({})); + const [$areAllGroupsDisabled] = useState(() => + computed($groupStatusMap, (groupStatusMap) => Object.values(groupStatusMap).every((status) => status === false)) + ); + + useEffect(() => { + const groupStatusMap = $groupStatusMap.get(); + const newMap: GroupStatusMap = {}; + for (const id of groupsWithOptions) { + if (initialGroupStates && initialGroupStates[id] !== undefined) { + newMap[id] = initialGroupStates[id]; + } else if (groupStatusMap[id] !== undefined) { + newMap[id] = groupStatusMap[id]; + } else { + newMap[id] = false; + } + } + $groupStatusMap.set(newMap); + }, [groupsWithOptions, $groupStatusMap, initialGroupStates]); + + const toggleGroup = useCallback( + (idToToggle: string) => { + const groupStatusMap = $groupStatusMap.get(); + const newMap: GroupStatusMap = {}; + for (const id of groupsWithOptions) { + const prevStatus = Boolean(groupStatusMap[id]); + newMap[id] = id === idToToggle ? !prevStatus : prevStatus; + } + $groupStatusMap.set(newMap); + }, + [$groupStatusMap, groupsWithOptions] + ); + + return { $groupStatusMap, $areAllGroupsDisabled, toggleGroup } as const; +}; + +const useKeyboardNavigation = () => { + const { getOptionId, $activeOptionId, $flattenedFilteredOptions, onSelectById, rootRef, onClose, inputRef } = + usePickerContext(); + + const setValueAndScrollIntoView = useCallback( + (id: string) => { + $activeOptionId.set(id); + const rootEl = rootRef.current; + if (!rootEl) { + return; + } + const itemEl = rootEl.querySelector(`#${CSS.escape(id)}`); + if (!itemEl) { + return; + } + itemEl.scrollIntoView({ block: 'nearest' }); + }, + [$activeOptionId, rootRef] + ); + + const prev = useCallback( + (e: React.KeyboardEvent) => { + e.preventDefault(); + const flattenedFilteredOptions = $flattenedFilteredOptions.get(); + const activeOptionId = $activeOptionId.get(); + if (flattenedFilteredOptions.length === 0) { + return; + } + if (e.metaKey) { + const item = flattenedFilteredOptions.at(0); + if (item) { + setValueAndScrollIntoView(getOptionId(item)); + } + return; + } + const currentIndex = flattenedFilteredOptions.findIndex((item) => getOptionId(item) === activeOptionId); + if (currentIndex < 0) { + return; + } + let newIndex = currentIndex - 1; + if (newIndex < 0) { + newIndex = flattenedFilteredOptions.length - 1; + } + const item = flattenedFilteredOptions.at(newIndex); + if (item) { + setValueAndScrollIntoView(getOptionId(item)); + } + }, + [$activeOptionId, $flattenedFilteredOptions, setValueAndScrollIntoView, getOptionId] + ); + + const next = useCallback( + (e: React.KeyboardEvent) => { + e.preventDefault(); + const activeOptionId = $activeOptionId.get(); + const flattenedFilteredOptions = $flattenedFilteredOptions.get(); + if (flattenedFilteredOptions.length === 0) { + return; + } + if (e.metaKey) { + const item = flattenedFilteredOptions.at(-1); + if (item) { + setValueAndScrollIntoView(getOptionId(item)); + } + return; + } + + const currentIndex = flattenedFilteredOptions.findIndex((item) => getOptionId(item) === activeOptionId); + if (currentIndex < 0) { + return; + } + let newIndex = currentIndex + 1; + if (newIndex >= flattenedFilteredOptions.length) { + newIndex = 0; + } + const item = flattenedFilteredOptions.at(newIndex); + if (item) { + setValueAndScrollIntoView(getOptionId(item)); + } + }, + [$activeOptionId, $flattenedFilteredOptions, setValueAndScrollIntoView, getOptionId] + ); + + const onKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'ArrowUp') { + prev(e); + } else if (e.key === 'ArrowDown') { + next(e); + } else if (e.key === 'Enter') { + const activeOptionId = $activeOptionId.get(); + if (!activeOptionId) { + return; + } + onSelectById(activeOptionId); + } else if (e.key === 'Escape') { + onClose?.(); + } else if (e.key === '/') { + e.preventDefault(); + inputRef.current?.focus(); + inputRef.current?.select(); + } + }, + [prev, next, $activeOptionId, onSelectById, onClose, inputRef] + ); + + const keyboardNavProps = useMemo(() => { + return { + onKeyDown, + }; + }, [onKeyDown]); + + return keyboardNavProps; +}; + +const useAtom = (initialValue: T) => { + return useState(() => atom(initialValue))[0]; +}; + +const useComputed = ( + stores: [...OriginStores], + cb: (...values: StoreValues) => Task | Value +) => { + return useState(() => computed(stores, cb))[0]; +}; + +const countOptions = (optionsOrGroups: OptionOrGroup[]) => { + let count = 0; + for (const optionOrGroup of optionsOrGroups) { + if (isGroup(optionOrGroup)) { + count += optionOrGroup.options.length; + } else { + count++; + } + } + return count; +}; + +export const Picker = typedMemo((props: PickerProps) => { + const { + pickerId, + getOptionId, + optionsOrGroups, + handleRef, + isMatch, + getIsOptionDisabled, + onClose, + onSelect, + selectedOption, + searchPlaceholder, + noMatchesFallback, + noOptionsFallback, + OptionComponent = DefaultOptionComponent, + NextToSearchBar, + searchable, + initialGroupStates, + } = props; + const rootRef = useRef(null); + const inputRef = useRef(null); + + const { $groupStatusMap, $areAllGroupsDisabled, toggleGroup } = useTogglableGroups( + optionsOrGroups, + initialGroupStates + ); + const $activeOptionId = useAtom(getFirstOptionId(optionsOrGroups, getOptionId)); + const $optionsOrGroups = useAtom(optionsOrGroups); + const $totalOptionCount = useComputed([$optionsOrGroups], countOptions); + const $filteredOptions = useAtom[]>([]); + const $flattenedFilteredOptions = useComputed([$filteredOptions], flattenOptions); + const $hasOptions = useComputed([$totalOptionCount], (count) => count > 0); + const $filteredOptionsCount = useComputed([$flattenedFilteredOptions], (options) => options.length); + const $hasFilteredOptions = useComputed([$filteredOptionsCount], (count) => count > 0); + const $selectedItem = useAtom(undefined); + const $searchTerm = useAtom(''); + const $selectedItemId = useComputed([$selectedItem], (item) => (item ? getOptionId(item) : undefined)); + + const selectIsCompactView = useMemo(() => buildSelectIsCompactView(pickerId), [pickerId]); + const isCompactView = useAppSelector(selectIsCompactView); + + const onSelectById = useCallback( + (id: string) => { + const options = $filteredOptions.get(); + const item = findOption(options, id, getOptionId); + if (!item) { + // Model not found? We should never get here. + return; + } + onSelect?.(item); + }, + [$filteredOptions, getOptionId, onSelect] + ); + + // Sync the picker's nanostores when props change + useEffect(() => { + $selectedItem.set(selectedOption); + }, [$selectedItem, selectedOption]); + + useEffect(() => { + $optionsOrGroups.set(optionsOrGroups); + }, [optionsOrGroups, $optionsOrGroups]); + + const ctx = useMemo( + () => + ({ + $optionsOrGroups, + $groupStatusMap, + isCompactView, + $activeOptionId, + $filteredOptions, + $flattenedFilteredOptions, + $totalOptionCount, + $selectedItem, + $searchTerm, + getOptionId, + isMatch, + getIsOptionDisabled, + onSelectById, + noOptionsFallback, + noMatchesFallback, + toggleGroup, + rootRef, + inputRef, + searchPlaceholder, + OptionComponent, + NextToSearchBar, + onClose, + searchable, + $areAllGroupsDisabled, + $selectedItemId, + $hasOptions, + $hasFilteredOptions, + $filteredOptionsCount, + pickerId, + }) satisfies PickerContextState, + [ + $optionsOrGroups, + $groupStatusMap, + isCompactView, + $activeOptionId, + $filteredOptions, + $flattenedFilteredOptions, + $totalOptionCount, + $selectedItem, + $searchTerm, + getOptionId, + isMatch, + getIsOptionDisabled, + onSelectById, + noOptionsFallback, + noMatchesFallback, + toggleGroup, + searchPlaceholder, + OptionComponent, + NextToSearchBar, + onClose, + searchable, + $areAllGroupsDisabled, + $selectedItemId, + $hasOptions, + $hasFilteredOptions, + $filteredOptionsCount, + pickerId, + ] + ); + + useImperativeHandle(handleRef, () => ctx, [ctx]); + + return ( + + + + + + + + + + + + ); +}); +Picker.displayName = 'Picker'; + +const PickerSyncer = typedMemo(() => { + const { + $optionsOrGroups, + $searchTerm, + $activeOptionId, + $groupStatusMap, + $areAllGroupsDisabled, + $filteredOptions, + searchable, + isMatch, + getOptionId, + } = usePickerContext(); + const searchTerm = useStore($searchTerm); + const groupStatusMap = useStore($groupStatusMap); + const areAllGroupsDisabled = useStore($areAllGroupsDisabled); + const optionsOrGroups = useStore($optionsOrGroups); + const [debouncedSearchTerm] = useDebounce(searchTerm, 300); + + useEffect(() => { + if (!debouncedSearchTerm || !searchable) { + const filtered = optionsOrGroups.filter((item) => { + if (isGroup(item)) { + return groupStatusMap[item.id] || areAllGroupsDisabled; + } else { + return true; + } + }); + $filteredOptions.set(filtered); + $activeOptionId.set(getFirstOptionId(filtered, getOptionId)); + } else { + const lowercasedSearchTerm = debouncedSearchTerm.toLowerCase(); + const filtered = []; + for (const item of optionsOrGroups) { + if (isGroup(item)) { + if (!groupStatusMap[item.id] && !areAllGroupsDisabled) { + continue; + } + const filteredItems = item.options.filter((item) => isMatch(item, lowercasedSearchTerm)); + if (filteredItems.length > 0) { + filtered.push({ ...item, options: filteredItems }); + } + } else { + if (isMatch(item, debouncedSearchTerm)) { + filtered.push(item); + } + } + } + $filteredOptions.set(filtered); + $activeOptionId.set(getFirstOptionId(filtered, getOptionId)); + } + }, [ + debouncedSearchTerm, + $activeOptionId, + getOptionId, + isMatch, + $filteredOptions, + searchable, + optionsOrGroups, + groupStatusMap, + areAllGroupsDisabled, + ]); + + return null; +}); +PickerSyncer.displayName = 'PickerSyncer'; + +const PickerContainer = typedMemo(({ children }: PropsWithChildren) => { + const { rootRef } = usePickerContext(); + const keyboardNavProps = useKeyboardNavigation(); + return ( + + {children} + + ); +}); +PickerContainer.displayName = 'PickerContainer'; + +const NoOptionsFallback = typedMemo(() => { + const { noOptionsFallback, $hasOptions } = usePickerContext(); + const hasOptions = useStore($hasOptions); + + if (hasOptions) { + return null; + } + + return {noOptionsFallback}; +}); +NoOptionsFallback.displayName = 'NoOptionsFallback'; + +const NoMatchesFallback = typedMemo(() => { + const { noMatchesFallback, $hasOptions, $hasFilteredOptions } = usePickerContext(); + + const hasOptions = useStore($hasOptions); + const hasFilteredOptions = useStore($hasFilteredOptions); + + if (!hasOptions) { + return null; + } + + if (hasFilteredOptions) { + return null; + } + + return {noMatchesFallback}; +}); +NoMatchesFallback.displayName = 'NoMatchesFallback'; + +const PickerSearchBar = typedMemo(() => { + const { NextToSearchBar, searchable } = usePickerContext(); + + if (!searchable) { + return null; + } + + return ( + + + + {NextToSearchBar} + + + + + ); +}); +PickerSearchBar.displayName = 'PickerSearchBar'; + +const SearchInput = typedMemo(() => { + const { inputRef, $totalOptionCount, $searchTerm, searchPlaceholder } = usePickerContext(); + const { t } = useTranslation(); + const searchTerm = useStore($searchTerm); + const totalOptionCount = useStore($totalOptionCount); + const placeholder = searchPlaceholder ?? t('common.search'); + const resetSearchTerm = useCallback(() => { + $searchTerm.set(''); + inputRef.current?.focus(); + }, [$searchTerm, inputRef]); + + const onChangeSearchTerm = useCallback( + (e: ChangeEvent) => { + $searchTerm.set(e.target.value); + }, + [$searchTerm] + ); + return ( + + + {searchTerm && ( + + } + isDisabled={totalOptionCount === 0} + disabled={false} + /> + + )} + + ); +}); +SearchInput.displayName = 'SearchInput'; +const GroupToggleButtons = typedMemo(() => { + const { $optionsOrGroups, $groupStatusMap, $areAllGroupsDisabled } = usePickerContext(); + const { t } = useTranslation(); + const $groups = useComputed([$optionsOrGroups], (optionsOrGroups) => { + const _groups: Group[] = []; + for (const optionOrGroup of optionsOrGroups) { + if (isGroup(optionOrGroup)) { + _groups.push(optionOrGroup); + } + } + return _groups; + }); + const groups = useStore($groups); + const areAllGroupsDisabled = useStore($areAllGroupsDisabled); + + const onClick = useCallback(() => { + const newMap: GroupStatusMap = {}; + for (const { id } of groups) { + newMap[id] = false; + } + $groupStatusMap.set(newMap); + }, [$groupStatusMap, groups]); + + if (!groups.length) { + return null; + } + + return ( + + {groups.map((group) => ( + + ))} + + } + aria-label={t('common.reset')} + tooltip={t('common.reset')} + size="sm" + variant="link" + alignSelf="stretch" + onClick={onClick} + // When a focused element is disabled, it blurs. This closes the popover. Fake the disabled state to prevent this. + // See: https://github.com/chakra-ui/chakra-ui/issues/7965 + opacity={areAllGroupsDisabled ? 0.5 : undefined} + pointerEvents={areAllGroupsDisabled ? 'none' : undefined} + /> + + ); +}); +GroupToggleButtons.displayName = 'GroupToggleButtons'; + +const CompactViewToggleButton = typedMemo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { isCompactView, pickerId } = usePickerContext(); + + const onClick = useCallback(() => { + if (pickerId) { + dispatch(pickerCompactViewStateChanged({ pickerId, isCompact: !isCompactView })); + } + }, [dispatch, pickerId, isCompactView]); + + const label = isCompactView ? t('common.fullView') : t('common.compactView'); + const icon = isCompactView ? : ; + + return ; +}); +CompactViewToggleButton.displayName = 'CompactViewToggleButton'; + +const GroupToggleButton = typedMemo(({ group }: { group: Group }) => { + const { toggleGroup, $groupStatusMap } = usePickerContext(); + const groupStatusMap = useStore($groupStatusMap); + + const onClick = useCallback(() => { + toggleGroup(group.id); + }, [group.id, toggleGroup]); + + const groupColor = getGroupColor(group); + const shortName = getGroupShortName(group); + const bg = groupStatusMap[group.id] ? groupColor : 'transparent'; + const color = groupStatusMap[group.id] ? undefined : 'base.200'; + + return ( + + {shortName} + + ); +}); +GroupToggleButton.displayName = 'GroupToggleButton'; + +const listSx = { + flexDir: 'column', + w: 'full', + gap: 2, + '&[data-is-compact="true"]': { + gap: 1, + }, +} satisfies SystemStyleObject; + +const PickerList = typedMemo(() => { + const { getOptionId, isCompactView, $filteredOptions } = usePickerContext(); + const filteredOptions = useStore($filteredOptions); + + if (filteredOptions.length === 0) { + return null; + } + + return ( + + + {filteredOptions.map((optionOrGroup, i) => { + if (isGroup(optionOrGroup)) { + const withDivider = !isCompactView && i < filteredOptions.length - 1; + return ( + + + {withDivider && } + + ); + } else { + const id = getOptionId(optionOrGroup); + return ; + } + })} + + + ); +}); +PickerList.displayName = 'PickerList'; + +const PickerGroup = typedMemo(({ group }: { group: Group }) => { + const { getOptionId, $groupStatusMap, $areAllGroupsDisabled } = usePickerContext(); + + const [$isGroupDisabled] = useState(() => + computed( + [$groupStatusMap, $areAllGroupsDisabled], + (groupStatusMap, areAllGroupsDisabled) => !groupStatusMap[group.id] && !areAllGroupsDisabled + ) + ); + const isGroupDisabled = useStore($isGroupDisabled); + + if (isGroupDisabled) { + return null; + } + + return ( + + {group.options.map((item) => { + const id = getOptionId(item); + return ; + })} + + ); +}); +PickerGroup.displayName = 'PickerGroup'; + +const PickerOption = typedMemo((props: { id: string; option: T }) => { + const { OptionComponent, $activeOptionId, $selectedItemId, onSelectById, getIsOptionDisabled } = + usePickerContext(); + const { id, option } = props; + const [$isActive] = useState(() => computed($activeOptionId, (activeOptionId) => activeOptionId === id)); + const [$isSelected] = useState(() => computed($selectedItemId, (selectedItemId) => selectedItemId === id)); + const isActive = useStore($isActive); + const isSelected = useStore($isSelected); + const setAsActive = useCallback(() => { + $activeOptionId.set(id); + }, [$activeOptionId, id]); + const select = useCallback(() => { + onSelectById(id); + }, [id, onSelectById]); + + const isDisabled = getIsOptionDisabled?.(option) ?? false; + const onPointerMove = isDisabled ? undefined : setAsActive; + const onClick = isDisabled ? undefined : select; + return ( + + ); +}); +PickerOption.displayName = 'PickerOption'; + +const getGroupColor = (group: Group) => { + return group.color ?? 'base.300'; +}; + +const getGroupShortName = (group: Group) => { + return group.shortName ?? group.name ?? group.id; +}; + +const getGroupName = (group: Group) => { + return group.name ?? group.id; +}; + +const getGroupCount = (group: Group, t: ReturnType['t']) => { + return ( + group.getOptionCountString?.(group.options.length) ?? t('common.options_withCount', { count: group.options.length }) + ); +}; + +const groupContainerSx = { + flexDir: 'column', + w: 'full', + borderLeftWidth: 4, + ps: 2, + '&[data-all-disabled="true"]': { + opacity: 0.5, + cursor: 'not-allowed', + }, +} satisfies SystemStyleObject; + +const PickerGroupContainer = typedMemo( + ({ group, children }: PropsWithChildren<{ group: Group }>) => { + const { getIsOptionDisabled } = usePickerContext(); + const color = getGroupColor(group); + const areAllDisabled = group.options.every((item) => getIsOptionDisabled?.(item) ?? false); + + return ( + + + + {children} + + + ); + } +); +PickerGroupContainer.displayName = 'PickerGroupContainer'; + +const groupHeaderSx = { + flexDir: 'column', + flex: 1, + ps: 2, + pe: 4, + py: 1, + userSelect: 'none', + position: 'sticky', + top: 0, + bg: 'base.800', + minH: 8, + '&[data-is-compact="true"]': { + ps: 1, + }, +} satisfies SystemStyleObject; + +const PickerGroupHeader = typedMemo(({ group }: { group: Group }) => { + const { t } = useTranslation(); + const { isCompactView } = usePickerContext(); + const color = getGroupColor(group); + const name = getGroupName(group); + const count = getGroupCount(group, t); + + return ( + + + + {name} + + + + {count} + + + + ); +}); +PickerGroupHeader.displayName = 'PickerGroupHeader'; diff --git a/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx b/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx new file mode 100644 index 00000000000..0018a78622c --- /dev/null +++ b/invokeai/frontend/web/src/common/components/SessionMenuItems.tsx @@ -0,0 +1,40 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { allEntitiesDeleted, inpaintMaskAdded } from 'features/controlLayers/store/canvasSlice'; +import { $canvasManager } from 'features/controlLayers/store/ephemeral'; +import { paramsReset } from 'features/controlLayers/store/paramsSlice'; +import { selectActiveTab } from 'features/ui/store/uiSelectors'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiArrowsCounterClockwiseBold } from 'react-icons/pi'; + +export const SessionMenuItems = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const tab = useAppSelector(selectActiveTab); + + const resetCanvasLayers = useCallback(() => { + dispatch(allEntitiesDeleted()); + dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true })); + $canvasManager.get()?.stage.fitBboxToStage(); + }, [dispatch]); + const resetGenerationSettings = useCallback(() => { + dispatch(paramsReset()); + }, [dispatch]); + return ( + <> + {tab === 'canvas' && ( + } onClick={resetCanvasLayers}> + {t('controlLayers.resetCanvasLayers')} + + )} + {(tab === 'canvas' || tab === 'generate') && ( + } onClick={resetGenerationSettings}> + {t('controlLayers.resetGenerationSettings')} + + )} + + ); +}); + +SessionMenuItems.displayName = 'SessionMenuItems'; diff --git a/invokeai/frontend/web/src/common/components/WavyLine.tsx b/invokeai/frontend/web/src/common/components/WavyLine.tsx new file mode 100644 index 00000000000..35acd789079 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/WavyLine.tsx @@ -0,0 +1,57 @@ +type Props = { + /** + * The amplitude of the wave. 0 is a straight line, higher values create more pronounced waves. + */ + amplitude: number; + /** + * The number of segments in the line. More segments create a smoother wave. + */ + segments?: number; + /** + * The color of the wave. + */ + stroke: string; + /** + * The width of the wave. + */ + strokeWidth: number; + /** + * The width of the SVG. + */ + width: number; + /** + * The height of the SVG. + */ + height: number; +}; + +const WavyLine = ({ amplitude, stroke, strokeWidth, width, height, segments = 5 }: Props) => { + // Calculate the path dynamically based on waviness + const generatePath = () => { + if (amplitude === 0) { + // If waviness is 0, return a straight line + return `M0,${height / 2} L${width},${height / 2}`; + } + + const clampedAmplitude = Math.min(height / 2, amplitude); // Cap amplitude to half the height + const segmentWidth = width / segments; + let path = `M0,${height / 2}`; // Start in the middle of the left edge + + // Loop through each segment and alternate the y position to create waves + for (let i = 1; i <= segments; i++) { + const x = i * segmentWidth; + const y = height / 2 + (i % 2 === 0 ? clampedAmplitude : -clampedAmplitude); + path += ` Q${x - segmentWidth / 2},${y} ${x},${height / 2}`; + } + + return path; + }; + + return ( + + + + ); +}; + +export default WavyLine; diff --git a/invokeai/frontend/web/src/common/components/linkify.ts b/invokeai/frontend/web/src/common/components/linkify.ts new file mode 100644 index 00000000000..4ac639468f7 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/linkify.ts @@ -0,0 +1,17 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import type { Opts as LinkifyOpts } from 'linkifyjs'; + +export const linkifySx: SystemStyleObject = { + a: { + fontWeight: 'semibold', + }, + 'a:hover': { + textDecoration: 'underline', + }, +}; + +export const linkifyOptions: LinkifyOpts = { + target: '_blank', + rel: 'noopener noreferrer', + validate: (value) => /^https?:\/\//.test(value), +}; diff --git a/invokeai/frontend/web/src/common/hooks/focus.test.ts b/invokeai/frontend/web/src/common/hooks/focus.test.ts new file mode 100644 index 00000000000..c106fe1cec4 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/focus.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; + +import { getFocusedRegion, setFocusedRegion } from './focus'; + +describe('focus regions', () => { + it('supports the workflows region', () => { + setFocusedRegion('workflows'); + expect(getFocusedRegion()).toBe('workflows'); + + setFocusedRegion(null); + expect(getFocusedRegion()).toBe(null); + }); +}); diff --git a/invokeai/frontend/web/src/common/hooks/focus.ts b/invokeai/frontend/web/src/common/hooks/focus.ts new file mode 100644 index 00000000000..4e093c5c631 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/focus.ts @@ -0,0 +1,195 @@ +import { useStore } from '@nanostores/react'; +import { logger } from 'app/logging/logger'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import type { Atom } from 'nanostores'; +import { atom, computed } from 'nanostores'; +import type { RefObject } from 'react'; +import { useEffect } from 'react'; +import { objectKeys } from 'tsafe'; + +/** + * We need to manage focus regions to conditionally enable hotkeys: + * - Some hotkeys should only be enabled when a specific region is focused. + * - Some hotkeys may conflict with other regions, so we need to disable them when a specific region is focused. For + * example, `esc` is used to clear the gallery selection, but it is also used to cancel a filter or transform on the + * canvas. + * + * To manage focus regions, we use a system of hooks and stores: + * - `useFocusRegion` is a hook that registers an element as part of a focus region. When that element is focused, by + * click or any other action, that region is set as the focused region. Optionally, focus can be set on mount. This + * is useful for components like the image viewer. + * - `useIsRegionFocused` is a hook that returns a boolean indicating if a specific region is focused. + * - `useFocusRegionWatcher` is a hook that listens for focus events on the window. When an element is focused, it + * checks if it is part of a focus region and sets that region as the focused region. + */ + +// + +const log = logger('system'); + +const REGION_NAMES = [ + 'launchpad', + 'viewer', + 'gallery', + 'boards', + 'layers', + 'canvas', + 'workflows', + 'progress', + 'settings', +] as const; +/** + * The names of the focus regions. + */ +export type FocusRegionName = (typeof REGION_NAMES)[number]; + +/** + * A map of focus regions to the elements that are part of that region. + */ +const REGION_TARGETS: Record> = REGION_NAMES.reduce( + (acc, region) => { + acc[region] = new Set(); + return acc; + }, + {} as Record> +); + +/** + * The currently-focused region or `null` if no region is focused. + */ +const $focusedRegion = atom(null); + +/** + * A map of focus regions to atoms that indicate if that region is focused. + */ +const FOCUS_REGIONS = objectKeys(REGION_TARGETS).reduce( + (acc, region) => { + acc[`$${region}`] = computed($focusedRegion, (focusedRegion) => focusedRegion === region); + return acc; + }, + {} as Record<`$${FocusRegionName}`, Atom> +); + +/** + * Sets the focused region, logging a trace level message. + */ +export const setFocusedRegion = (region: FocusRegionName | null) => { + $focusedRegion.set(region); + log.trace(`Focus changed: ${region}`); +}; + +export const getFocusedRegion = () => $focusedRegion.get(); + +type UseFocusRegionOptions = { + focusOnMount?: boolean; +}; + +/** + * Registers an element as part of a focus region. When that element is focused, by click or any other action, that + * region is set as the focused region. Optionally, focus can be set on mount. + * + * On unmount, if the element is the last element in the region and the region is focused, the focused region is set to + * `null`. + * + * @param region The focus region name. + * @param ref The ref of the element to register. + * @param options The options. + */ +export const useFocusRegion = ( + region: FocusRegionName, + ref: RefObject, + options?: UseFocusRegionOptions +) => { + useEffect(() => { + if (!ref.current) { + return; + } + + const { focusOnMount = false } = { focusOnMount: false, ...options }; + + const element = ref.current; + + REGION_TARGETS[region].add(element); + + if (focusOnMount) { + setFocusedRegion(region); + } + + return () => { + REGION_TARGETS[region].delete(element); + + if (REGION_TARGETS[region].size === 0 && $focusedRegion.get() === region) { + setFocusedRegion(null); + } + }; + }, [options, ref, region]); +}; + +/** + * Returns a boolean indicating if a specific region is focused. + * @param region The focus region name. + */ +export const useIsRegionFocused = (region: FocusRegionName) => { + return useStore(FOCUS_REGIONS[`$${region}`]); +}; + +/** + * Listens for focus events on the window. When an element is focused, it checks if it is part of a focus region and sets + * that region as the focused region. The region corresponding to the deepest element is set. + */ +const onFocus = (_: FocusEvent) => { + const activeElement = document.activeElement; + if (!(activeElement instanceof HTMLElement)) { + return; + } + + const regionCandidates: { region: FocusRegionName; element: HTMLElement }[] = []; + + for (const region of objectKeys(REGION_TARGETS)) { + for (const element of REGION_TARGETS[region]) { + if (element.contains(activeElement)) { + regionCandidates.push({ region, element }); + } + } + } + + if (regionCandidates.length === 0) { + return; + } + + // Sort by the shallowest element + regionCandidates.sort((a, b) => { + if (b.element.contains(a.element)) { + return -1; + } + if (a.element.contains(b.element)) { + return 1; + } + return 0; + }); + + // Set the region of the deepest element + const focusedRegion = regionCandidates[0]?.region; + + if (!focusedRegion) { + log.warn('No focused region found'); + return; + } + + setFocusedRegion(focusedRegion); +}; + +/** + * Listens for focus events on the window. When an element is focused, it checks if it is part of a focus region and sets + * that region as the focused region. This is a singleton. + */ +export const useFocusRegionWatcher = () => { + useAssertSingleton('useFocusRegionWatcher'); + + useEffect(() => { + window.addEventListener('focus', onFocus, { capture: true }); + return () => { + window.removeEventListener('focus', onFocus, { capture: true }); + }; + }, []); +}; diff --git a/invokeai/frontend/web/src/common/hooks/useAssertSingleton.ts b/invokeai/frontend/web/src/common/hooks/useAssertSingleton.ts new file mode 100644 index 00000000000..0f7cc9db6f5 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useAssertSingleton.ts @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; +import { assert } from 'tsafe'; + +const IDS = new Set(); + +/** + * Asserts that there is only one instance of a singleton entity. It can be a hook or a component. + * @param id The ID of the singleton entity. + */ +export function useAssertSingleton(id: string) { + useEffect(() => { + assert(!IDS.has(id), `There should be only one instance of ${id}`); + IDS.add(id); + return () => { + IDS.delete(id); + }; + }, [id]); +} diff --git a/invokeai/frontend/web/src/common/hooks/useAsyncState.ts b/invokeai/frontend/web/src/common/hooks/useAsyncState.ts new file mode 100644 index 00000000000..61291aa1ecd --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useAsyncState.ts @@ -0,0 +1,115 @@ +import { useStore } from '@nanostores/react'; +import { WrappedError } from 'common/util/result'; +import type { Atom } from 'nanostores'; +import { atom } from 'nanostores'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +type SuccessState = { + status: 'success'; + value: T; + error: null; +}; + +type ErrorState = { + status: 'error'; + value: null; + error: Error; +}; + +type PendingState = { + status: 'pending'; + value: null; + error: null; +}; + +type IdleState = { + status: 'idle'; + value: null; + error: null; +}; + +export type State = IdleState | PendingState | SuccessState | ErrorState; + +type UseAsyncStateOptions = { + immediate?: boolean; +}; + +type UseAsyncReturn = { + $state: Atom>; + trigger: () => Promise; + reset: () => void; +}; + +export const useAsyncState = (execute: () => Promise, options?: UseAsyncStateOptions): UseAsyncReturn => { + const $state = useState(() => + atom>({ + status: 'idle', + value: null, + error: null, + }) + )[0]; + + const trigger = useCallback(async () => { + $state.set({ + status: 'pending', + value: null, + error: null, + }); + try { + const value = await execute(); + $state.set({ + status: 'success', + value, + error: null, + }); + } catch (error) { + $state.set({ + status: 'error', + value: null, + error: WrappedError.wrap(error), + }); + } + }, [$state, execute]); + + const reset = useCallback(() => { + $state.set({ + status: 'idle', + value: null, + error: null, + }); + }, [$state]); + + useEffect(() => { + if (options?.immediate) { + trigger(); + } + }, [options?.immediate, trigger]); + + const api = useMemo( + () => + ({ + $state, + trigger, + reset, + }) satisfies UseAsyncReturn, + [$state, trigger, reset] + ); + + return api; +}; + +type UseAsyncReturnReactive = { + state: State; + trigger: () => Promise; + reset: () => void; +}; + +export const useAsyncStateReactive = ( + execute: () => Promise, + options?: UseAsyncStateOptions +): UseAsyncReturnReactive => { + const { $state, trigger, reset } = useAsyncState(execute, options); + const state = useStore($state); + + return { state, trigger, reset }; +}; diff --git a/invokeai/frontend/web/src/common/hooks/useBoolean.ts b/invokeai/frontend/web/src/common/hooks/useBoolean.ts new file mode 100644 index 00000000000..ec68457ecdd --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useBoolean.ts @@ -0,0 +1,151 @@ +import { useStore } from '@nanostores/react'; +import type { WritableAtom } from 'nanostores'; +import { atom } from 'nanostores'; +import { useCallback, useState } from 'react'; + +type UseBoolean = { + isTrue: boolean; + setTrue: () => void; + setFalse: () => void; + set: (value: boolean) => void; + toggle: () => void; +}; + +/** + * Creates a hook to manage a boolean state. The boolean is stored in a nanostores atom. + * Returns a tuple containing the hook and the atom. Use this for global boolean state. + * @param initialValue Initial value of the boolean + */ +export const buildUseBoolean = (initialValue: boolean): [() => UseBoolean, WritableAtom] => { + const $boolean = atom(initialValue); + + const setTrue = () => { + $boolean.set(true); + }; + const setFalse = () => { + $boolean.set(false); + }; + const set = (value: boolean) => { + $boolean.set(value); + }; + const toggle = () => { + $boolean.set(!$boolean.get()); + }; + + const useBoolean = () => { + const isTrue = useStore($boolean); + + return { + isTrue, + setTrue, + setFalse, + set, + toggle, + }; + }; + + return [useBoolean, $boolean] as const; +}; + +/** + * Hook to manage a boolean state. Use this for a local boolean state. + * @param initialValue Initial value of the boolean + */ +export const useBoolean = (initialValue: boolean): UseBoolean => { + const [isTrue, set] = useState(initialValue); + + const setTrue = useCallback(() => { + set(true); + }, [set]); + const setFalse = useCallback(() => { + set(false); + }, [set]); + const toggle = useCallback(() => { + set((val) => !val); + }, [set]); + + return { + isTrue, + setTrue, + setFalse, + set, + toggle, + }; +}; + +type UseDisclosure = { + isOpen: boolean; + open: () => void; + close: () => void; + set: (isOpen: boolean) => void; + toggle: () => void; +}; + +/** + * This is the same as `buildUseBoolean`, but the method names are more descriptive, + * serving the semantics of a disclosure state. + * + * Creates a hook to manage a boolean state. The boolean is stored in a nanostores atom. + * Returns a tuple containing the hook and the atom. Use this for global boolean state. + * + * @param defaultIsOpen Initial state of the disclosure + */ +export const buildUseDisclosure = (defaultIsOpen: boolean): [() => UseDisclosure, WritableAtom] => { + const $isOpen = atom(defaultIsOpen); + + const open = () => { + $isOpen.set(true); + }; + const close = () => { + $isOpen.set(false); + }; + const set = (isOpen: boolean) => { + $isOpen.set(isOpen); + }; + const toggle = () => { + $isOpen.set(!$isOpen.get()); + }; + + const useDisclosure = () => { + const isOpen = useStore($isOpen); + + return { + isOpen, + open, + close, + set, + toggle, + }; + }; + + return [useDisclosure, $isOpen] as const; +}; + +/** + * This is the same as `useBoolean`, but the method names are more descriptive, + * serving the semantics of a disclosure state. + * + * Hook to manage a boolean state. Use this for a local boolean state. + * @param defaultIsOpen Initial state of the disclosure + */ +export const useDisclosure = (defaultIsOpen: boolean): UseDisclosure => { + const [isOpen, set] = useState(defaultIsOpen); + + const open = useCallback(() => { + set(true); + }, [set]); + const close = useCallback(() => { + set(false); + }, [set]); + const toggle = useCallback(() => { + set((val) => !val); + }, [set]); + + return { + isOpen, + open, + close, + set, + toggle, + }; +}; diff --git a/invokeai/frontend/web/src/common/hooks/useCallbackOnDragEnter.ts b/invokeai/frontend/web/src/common/hooks/useCallbackOnDragEnter.ts new file mode 100644 index 00000000000..f0a1c2ed743 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useCallbackOnDragEnter.ts @@ -0,0 +1,29 @@ +import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; +import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; +import { dropTargetForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter'; +import { useTimeoutCallback } from 'common/hooks/useTimeoutCallback'; +import type { RefObject } from 'react'; +import { useEffect } from 'react'; + +export const useCallbackOnDragEnter = (cb: () => void, ref: RefObject, delay = 300) => { + const [run, cancel] = useTimeoutCallback(cb, delay); + + useEffect(() => { + const element = ref.current; + if (!element) { + return; + } + return combine( + dropTargetForElements({ + element, + onDragEnter: run, + onDragLeave: cancel, + }), + dropTargetForExternal({ + element, + onDragEnter: run, + onDragLeave: cancel, + }) + ); + }, [cancel, ref, run]); +}; diff --git a/invokeai/frontend/web/src/common/hooks/useClipboard.tsx b/invokeai/frontend/web/src/common/hooks/useClipboard.tsx new file mode 100644 index 00000000000..918f2cc781e --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useClipboard.tsx @@ -0,0 +1,81 @@ +/* eslint-disable no-restricted-properties */ + +import { ExternalLink, Text } from '@invoke-ai/ui-library'; +import { toast } from 'features/toast/toast'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import type { Param0 } from 'tsafe'; + +const CLIPBOARD_FAQ_URL = 'https://invoke.ai/troubleshooting/faq/#unable-to-copy-on-firefox'; + +export const useClipboard = () => { + const { t } = useTranslation(); + const alertClipboardNotAvailable = useCallback(() => { + toast({ + id: 'CLIPBOARD_UNAVAILABLE', + title: t('toast.unableToCopy'), + description: ( + <> + + {t('toast.unableToCopyDesc')} + + . + + + ), + status: 'error', + }); + }, [t]); + + const isAvailable = useMemo(() => { + if (!navigator.clipboard || !window.ClipboardItem) { + return false; + } + // TODO(psyche): Should we query the permissions API? + return true; + }, []); + + const writeText = useCallback( + (data: Param0, onCopy?: () => void) => { + if (!isAvailable) { + alertClipboardNotAvailable(); + return; + } + navigator.clipboard.writeText(data); + onCopy?.(); + }, + [alertClipboardNotAvailable, isAvailable] + ); + + const write = useCallback( + (data: Param0, onCopy?: () => void) => { + if (!isAvailable) { + alertClipboardNotAvailable(); + return; + } + navigator.clipboard.write(data); + onCopy?.(); + }, + [alertClipboardNotAvailable, isAvailable] + ); + + const writeImage = useCallback( + (blob: Blob, onCopy?: () => void) => { + if (!isAvailable) { + alertClipboardNotAvailable(); + return; + } + const data = [new ClipboardItem({ ['image/png']: blob })]; + navigator.clipboard.write(data); + onCopy?.(); + }, + [alertClipboardNotAvailable, isAvailable] + ); + + return { isAvailable, writeText, write, writeImage }; +}; diff --git a/invokeai/frontend/web/src/common/hooks/useCloseChakraTooltipsOnDragFix.ts b/invokeai/frontend/web/src/common/hooks/useCloseChakraTooltipsOnDragFix.ts new file mode 100644 index 00000000000..79a92398d79 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useCloseChakraTooltipsOnDragFix.ts @@ -0,0 +1,19 @@ +import { useEffect } from 'react'; + +// Chakra tooltips sometimes open during a drag operation. We can fix it by dispatching an event that chakra listens +// for to close tooltips. It's reaching into the internals but it seems to work. + +const closeEventName = 'chakra-ui:close-tooltip'; + +export const useCloseChakraTooltipsOnDragFix = () => { + useEffect(() => { + const closeTooltips = () => { + document.dispatchEvent(new window.CustomEvent(closeEventName)); + }; + document.addEventListener('drag', closeTooltips); + + return () => { + document.removeEventListener('drag', closeTooltips); + }; + }, []); +}; diff --git a/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts b/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts new file mode 100644 index 00000000000..e46227b2f5d --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts @@ -0,0 +1,40 @@ +import { useClipboard } from 'common/hooks/useClipboard'; +import { convertImageUrlToBlob } from 'common/util/convertImageUrlToBlob'; +import { toast } from 'features/toast/toast'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const useCopyImageToClipboard = () => { + const { t } = useTranslation(); + const clipboard = useClipboard(); + + const copyImageToClipboard = useCallback( + async (image_url: string) => { + try { + const blob = await convertImageUrlToBlob(image_url); + + if (!blob) { + throw new Error('Unable to create Blob'); + } + + clipboard.writeImage(blob, () => { + toast({ + id: 'IMAGE_COPIED', + title: t('toast.imageCopied'), + status: 'success', + }); + }); + } catch (err) { + toast({ + id: 'PROBLEM_COPYING_IMAGE', + title: t('toast.problemCopyingImage'), + description: String(err), + status: 'error', + }); + } + }, + [clipboard, t] + ); + + return copyImageToClipboard; +}; diff --git a/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts b/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts new file mode 100644 index 00000000000..33b90e1d7fe --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts @@ -0,0 +1,37 @@ +import { toast } from 'features/toast/toast'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const useDownloadItem = () => { + const { t } = useTranslation(); + + const downloadItem = useCallback( + async (item_url: string, item_id: string) => { + try { + const blob = await fetch(item_url).then((resp) => resp.blob()); + if (!blob) { + throw new Error('Unable to create Blob'); + } + + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = item_id; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + } catch (err) { + toast({ + id: 'PROBLEM_DOWNLOADING_IMAGE', + title: t('toast.problemDownloadingImage'), + description: String(err), + status: 'error', + }); + } + }, + [t] + ); + + return { downloadItem }; +}; diff --git a/invokeai/frontend/web/src/common/hooks/useEditable.ts b/invokeai/frontend/web/src/common/hooks/useEditable.ts new file mode 100644 index 00000000000..10b8694a679 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useEditable.ts @@ -0,0 +1,72 @@ +import type { ChangeEvent, KeyboardEvent, RefObject } from 'react'; +import { useCallback, useEffect, useState } from 'react'; + +type UseEditableArg = { + value: string; + defaultValue: string; + onChange: (value: string) => void; + onStartEditing?: () => void; + inputRef?: RefObject; +}; + +export const useEditable = ({ value, defaultValue, onChange: _onChange, onStartEditing, inputRef }: UseEditableArg) => { + const [isEditing, setIsEditing] = useState(false); + const [localValue, setLocalValue] = useState(value); + + const onBlur = useCallback(() => { + const trimmedValue = localValue.trim(); + const newValue = trimmedValue || defaultValue; + setLocalValue(newValue); + if (newValue !== value) { + _onChange(newValue); + } + setIsEditing(false); + inputRef?.current?.setSelectionRange(0, 0); + }, [localValue, defaultValue, value, inputRef, _onChange]); + + const onChange = useCallback((e: ChangeEvent) => { + setLocalValue(e.target.value); + }, []); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + onBlur(); + } else if (e.key === 'Escape') { + setLocalValue(value); + _onChange(value); + setIsEditing(false); + } + }, + [_onChange, onBlur, value] + ); + + const startEditing = useCallback(() => { + setIsEditing(true); + onStartEditing?.(); + }, [onStartEditing]); + + useEffect(() => { + // Another component may change the title; sync local title with global state + setLocalValue(value); + }, [value]); + + useEffect(() => { + if (isEditing) { + inputRef?.current?.focus(); + inputRef?.current?.select(); + } + }, [inputRef, isEditing]); + + return { + isEditing, + startEditing, + value: localValue, + inputProps: { + value: localValue, + onChange, + onKeyDown, + onBlur, + }, + }; +}; diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts new file mode 100644 index 00000000000..dd43c0b0947 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts @@ -0,0 +1,147 @@ +import { useAppStore } from 'app/store/storeHooks'; +import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state'; +import { selectSelection } from 'features/gallery/store/gallerySelectors'; +import { useClearQueue } from 'features/queue/hooks/useClearQueue'; +import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem'; +import { useInvoke } from 'features/queue/hooks/useInvoke'; +import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; +import { navigationApi } from 'features/ui/layouts/navigation-api'; + +import { getFocusedRegion } from './focus'; + +export const useGlobalHotkeys = () => { + const { dispatch, getState } = useAppStore(); + const queue = useInvoke(); + + useRegisteredHotkeys({ + id: 'invoke', + category: 'app', + callback: queue.enqueueBack, + options: { + enabled: !queue.isDisabled && !queue.isLoading, + preventDefault: true, + enableOnFormTags: ['input', 'textarea', 'select'], + }, + dependencies: [queue], + }); + + useRegisteredHotkeys({ + id: 'invokeFront', + category: 'app', + callback: queue.enqueueFront, + options: { + enabled: !queue.isDisabled && !queue.isLoading, + preventDefault: true, + enableOnFormTags: ['input', 'textarea', 'select'], + }, + dependencies: [queue], + }); + + const deleteCurrentQueueItem = useDeleteCurrentQueueItem(); + + useRegisteredHotkeys({ + id: 'cancelQueueItem', + category: 'app', + callback: deleteCurrentQueueItem.trigger, + options: { + enabled: !deleteCurrentQueueItem.isDisabled && !deleteCurrentQueueItem.isLoading, + preventDefault: true, + }, + dependencies: [deleteCurrentQueueItem], + }); + + const clearQueue = useClearQueue(); + + useRegisteredHotkeys({ + id: 'clearQueue', + category: 'app', + callback: clearQueue.trigger, + options: { + enabled: !clearQueue.isDisabled && !clearQueue.isLoading, + preventDefault: true, + }, + dependencies: [clearQueue], + }); + + useRegisteredHotkeys({ + id: 'selectGenerateTab', + category: 'app', + callback: () => { + navigationApi.switchToTab('generate'); + }, + dependencies: [dispatch], + }); + + useRegisteredHotkeys({ + id: 'selectCanvasTab', + category: 'app', + callback: () => { + navigationApi.switchToTab('canvas'); + }, + dependencies: [dispatch], + }); + + useRegisteredHotkeys({ + id: 'selectUpscalingTab', + category: 'app', + callback: () => { + navigationApi.switchToTab('upscaling'); + }, + dependencies: [dispatch], + }); + + useRegisteredHotkeys({ + id: 'selectWorkflowsTab', + category: 'app', + callback: () => { + navigationApi.switchToTab('workflows'); + }, + dependencies: [dispatch], + }); + + useRegisteredHotkeys({ + id: 'selectModelsTab', + category: 'app', + callback: () => { + navigationApi.switchToTab('models'); + }, + dependencies: [dispatch], + }); + + useRegisteredHotkeys({ + id: 'selectQueueTab', + category: 'app', + callback: () => { + navigationApi.switchToTab('queue'); + }, + dependencies: [dispatch], + }); + + const deleteImageModalApi = useDeleteImageModalApi(); + + useRegisteredHotkeys({ + id: 'deleteSelection', + category: 'gallery', + callback: () => { + const focusedRegion = getFocusedRegion(); + if (focusedRegion !== 'gallery' && focusedRegion !== 'viewer') { + return; + } + const selection = selectSelection(getState()); + if (!selection.length) { + return; + } + deleteImageModalApi.delete(selection); + }, + dependencies: [getState, deleteImageModalApi], + }); + + useRegisteredHotkeys({ + id: 'toggleViewer', + category: 'viewer', + callback: () => { + navigationApi.toggleViewerPanel(); + }, + dependencies: [], + }); +}; diff --git a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts new file mode 100644 index 00000000000..a7f8c812af2 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts @@ -0,0 +1,105 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import type { GroupBase } from 'chakra-react-select'; +import { groupBy, reduce } from 'es-toolkit/compat'; +import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import type { ModelIdentifierField } from 'features/nodes/types/common'; +import { selectSystemShouldEnableModelDescriptions } from 'features/system/store/systemSlice'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import type { AnyModelConfig } from 'services/api/types'; + +type UseGroupedModelComboboxArg = { + modelConfigs: T[]; + selectedModel?: ModelIdentifierField | null; + onChange: (value: T | null) => void; + getIsDisabled?: (model: T) => boolean; + isLoading?: boolean; + groupByType?: boolean; +}; + +type UseGroupedModelComboboxReturn = { + value: ComboboxOption | undefined | null; + options: GroupBase[]; + onChange: ComboboxOnChange; + placeholder: string; + noOptionsMessage: () => string; +}; + +const groupByBaseFunc = (model: T) => model.base.toUpperCase(); +const groupByBaseAndTypeFunc = (model: T) => + `${model.base.toUpperCase()} / ${model.type.replaceAll('_', ' ').toUpperCase()}`; + +const selectBaseWithSDXLFallback = createSelector(selectParamsSlice, (params) => params.model?.base ?? 'sdxl'); + +export const useGroupedModelCombobox = ( + arg: UseGroupedModelComboboxArg +): UseGroupedModelComboboxReturn => { + const { t } = useTranslation(); + const base = useAppSelector(selectBaseWithSDXLFallback); + const shouldShowModelDescriptions = useAppSelector(selectSystemShouldEnableModelDescriptions); + const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, groupByType = false } = arg; + const options = useMemo[]>(() => { + if (!modelConfigs) { + return []; + } + const groupedModels = groupBy(modelConfigs, groupByType ? groupByBaseAndTypeFunc : groupByBaseFunc); + const _options = reduce( + groupedModels, + (acc, val, label) => { + acc.push({ + label, + options: val.map((model) => ({ + label: model.name, + value: model.key, + description: (shouldShowModelDescriptions && model.description) || undefined, + isDisabled: getIsDisabled ? getIsDisabled(model) : false, + })), + }); + return acc; + }, + [] as GroupBase[] + ); + _options.sort((a) => (a.label?.split('/')[0]?.toLowerCase().includes(base) ? -1 : 1)); + return _options; + }, [modelConfigs, groupByType, getIsDisabled, base, shouldShowModelDescriptions]); + + const value = useMemo( + () => + options.flatMap((o) => o.options).find((m) => (selectedModel ? m.value === selectedModel.key : false)) ?? null, + [options, selectedModel] + ); + + const _onChange = useCallback( + (v) => { + if (!v) { + onChange(null); + return; + } + const model = modelConfigs.find((m) => m.key === v.value); + if (!model) { + onChange(null); + return; + } + onChange(model); + }, + [modelConfigs, onChange] + ); + + const placeholder = useMemo(() => { + if (isLoading) { + return t('common.loading'); + } + + if (options.length === 0) { + return t('models.noModelsAvailable'); + } + + return t('models.selectModel'); + }, [isLoading, options, t]); + + const noOptionsMessage = useCallback(() => t('models.noMatchingModels'), [t]); + + return { options, value, onChange: _onChange, placeholder, noOptionsMessage }; +}; diff --git a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx new file mode 100644 index 00000000000..fc173de979f --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx @@ -0,0 +1,266 @@ +import type { ButtonProps, IconButtonProps, SystemStyleObject } from '@invoke-ai/ui-library'; +import { Button, IconButton } from '@invoke-ai/ui-library'; +import { logger } from 'app/logging/logger'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; +import { toast } from 'features/toast/toast'; +import { memo, useCallback } from 'react'; +import type { Accept, FileRejection } from 'react-dropzone'; +import { useDropzone } from 'react-dropzone'; +import { useTranslation } from 'react-i18next'; +import { PiUploadBold } from 'react-icons/pi'; +import { uploadImages, useUploadImageMutation } from 'services/api/endpoints/images'; +import type { ImageDTO } from 'services/api/types'; +import { assert } from 'tsafe'; +import type { SetOptional } from 'type-fest'; + +const addUpperCaseReducer = (acc: string[], ext: string) => { + acc.push(ext); + acc.push(ext.toUpperCase()); + return acc; +}; + +export const dropzoneAccept: Accept = { + 'image/png': ['.png'].reduce(addUpperCaseReducer, [] as string[]), + 'image/jpeg': ['.jpg', '.jpeg', '.png'].reduce(addUpperCaseReducer, [] as string[]), + 'image/webp': ['.webp'].reduce(addUpperCaseReducer, [] as string[]), +}; + +type UseImageUploadButtonArgs = + | { + isDisabled?: boolean; + allowMultiple: false; + onUpload?: (imageDTO: ImageDTO) => void; + onUploadStarted?: (files: File) => void; + onError?: (error: unknown) => void; + } + | { + isDisabled?: boolean; + allowMultiple: true; + onUpload?: (imageDTOs: ImageDTO[]) => void; + onUploadStarted?: (files: File[]) => void; + onError?: (error: unknown) => void; + }; + +const log = logger('gallery'); + +/** + * Provides image uploader functionality to any component. + * + * @example + * const { getUploadButtonProps, getUploadInputProps, openUploader } = useImageUploadButton({ + * postUploadAction: { + * type: 'SET_CONTROL_ADAPTER_IMAGE', + * controlNetId: '12345', + * }, + * isDisabled: getIsUploadDisabled(), + * }); + * + * // open the uploaded directly + * const handleSomething = () => { openUploader() } + * + * // in the render function + * + + + ); +}); +UploadImageButton.displayName = 'UploadImageButton'; + +export const UploadMultipleImageButton = ({ + isDisabled = false, + onUpload, + isError = false, + ...rest +}: { + onUpload?: (imageDTOs: ImageDTO[]) => void; + isError?: boolean; +} & SetOptional) => { + const { t } = useTranslation(); + const uploadApi = useImageUploadButton({ isDisabled, allowMultiple: true, onUpload }); + return ( + <> + } + isLoading={uploadApi.request.isLoading} + {...rest} + {...uploadApi.getUploadButtonProps()} + /> + + + ); +}; diff --git a/invokeai/frontend/web/src/common/hooks/useMiddleClickOpenInNewTab.ts b/invokeai/frontend/web/src/common/hooks/useMiddleClickOpenInNewTab.ts new file mode 100644 index 00000000000..56f58e26a38 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useMiddleClickOpenInNewTab.ts @@ -0,0 +1,72 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { openImageInNewTab } from 'common/util/openImageInNewTab'; +import { selectSystemShouldUseMiddleClickToOpenInNewTab } from 'features/system/store/systemSlice'; +import type { RefObject } from 'react'; +import { useEffect } from 'react'; + +type Options = { + requireDirectTarget?: boolean; +}; + +const shouldHandleMiddleClick = ( + event: MouseEvent, + element: T, + requireDirectTarget: boolean +) => { + if (event.button !== 1) { + return false; + } + + if (requireDirectTarget && event.target !== element) { + return false; + } + + return true; +}; + +export const useMiddleClickOpenInNewTab = ( + ref: RefObject, + imageUrl: string, + { requireDirectTarget = false }: Options = {} +) => { + const shouldUseMiddleClickToOpenInNewTab = useAppSelector(selectSystemShouldUseMiddleClickToOpenInNewTab); + + useEffect(() => { + const element = ref.current; + + if (!element || !shouldUseMiddleClickToOpenInNewTab) { + return; + } + + // If auxclick is unsupported, leave the browser's default middle-click behavior intact. + if (!('onauxclick' in element)) { + return; + } + + const onMouseDown = (event: MouseEvent) => { + if (!shouldHandleMiddleClick(event, element, requireDirectTarget)) { + return; + } + + event.preventDefault(); + }; + + const onAuxClick = (event: MouseEvent) => { + if (!shouldHandleMiddleClick(event, element, requireDirectTarget)) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + openImageInNewTab(imageUrl); + }; + + element.addEventListener('mousedown', onMouseDown); + element.addEventListener('auxclick', onAuxClick); + + return () => { + element.removeEventListener('mousedown', onMouseDown); + element.removeEventListener('auxclick', onAuxClick); + }; + }, [imageUrl, ref, requireDirectTarget, shouldUseMiddleClickToOpenInNewTab]); +}; diff --git a/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts new file mode 100644 index 00000000000..b79ca940e0b --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts @@ -0,0 +1,76 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import type { ModelIdentifierField } from 'features/nodes/types/common'; +import { selectSystemShouldEnableModelDescriptions } from 'features/system/store/systemSlice'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import type { AnyModelConfig } from 'services/api/types'; + +type UseModelComboboxArg = { + modelConfigs: T[]; + selectedModel?: ModelIdentifierField | null; + onChange: (value: T | null) => void; + getIsDisabled?: (model: T) => boolean; + optionsFilter?: (model: T) => boolean; + isLoading?: boolean; +}; + +type UseModelComboboxReturn = { + value: ComboboxOption | undefined | null; + options: ComboboxOption[]; + onChange: ComboboxOnChange; + placeholder: string; + noOptionsMessage: () => string; +}; + +export const useModelCombobox = (arg: UseModelComboboxArg): UseModelComboboxReturn => { + const { t } = useTranslation(); + const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, optionsFilter = () => true } = arg; + const shouldShowModelDescriptions = useAppSelector(selectSystemShouldEnableModelDescriptions); + + const options = useMemo(() => { + return modelConfigs.filter(optionsFilter).map((model) => ({ + label: model.name, + value: model.key, + description: (shouldShowModelDescriptions && model.description) || undefined, + isDisabled: getIsDisabled ? getIsDisabled(model) : false, + })); + }, [optionsFilter, getIsDisabled, modelConfigs, shouldShowModelDescriptions]); + + const value = useMemo( + () => options.find((m) => (selectedModel ? m.value === selectedModel.key : false)) ?? null, + [options, selectedModel] + ); + + const _onChange = useCallback( + (v) => { + if (!v) { + onChange(null); + return; + } + const model = modelConfigs.find((m) => m.key === v.value); + if (!model) { + onChange(null); + return; + } + onChange(model); + }, + [modelConfigs, onChange] + ); + + const placeholder = useMemo(() => { + if (isLoading) { + return t('common.loading'); + } + + if (options.length === 0) { + return t('models.noModelsAvailable'); + } + + return t('models.selectModel'); + }, [isLoading, options, t]); + + const noOptionsMessage = useCallback(() => t('models.noMatchingModels'), [t]); + + return { options, value, onChange: _onChange, placeholder, noOptionsMessage }; +}; diff --git a/invokeai/frontend/web/src/common/hooks/usePersistedTextareaSize.ts b/invokeai/frontend/web/src/common/hooks/usePersistedTextareaSize.ts new file mode 100644 index 00000000000..2a2dc2008f0 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/usePersistedTextareaSize.ts @@ -0,0 +1,108 @@ +import { useAppStore } from 'app/store/storeHooks'; +import { debounce } from 'es-toolkit/compat'; +import type { Dimensions } from 'features/controlLayers/store/types'; +import { selectUiSlice, textAreaSizesStateChanged } from 'features/ui/store/uiSlice'; +import { type RefObject, useCallback, useEffect, useMemo } from 'react'; + +type Options = { + trackWidth: boolean; + trackHeight: boolean; + initialWidth?: number; + initialHeight?: number; +}; + +/** + * Persists the width and/or height of a text area to redux. + * @param id The unique id of this textarea, used as key to storage + * @param ref A ref to the textarea element + * @param options.trackWidth Whether to track width + * @param options.trackHeight Whether to track width + * @param options.initialWidth An optional initial width in pixels + * @param options.initialHeight An optional initial height in pixels + */ +export const usePersistedTextAreaSize = (id: string, ref: RefObject, options: Options) => { + const { dispatch, getState } = useAppStore(); + + const onResize = useCallback( + (size: Partial) => { + dispatch(textAreaSizesStateChanged({ id, size })); + }, + [dispatch, id] + ); + + const debouncedOnResize = useMemo(() => debounce(onResize, 300), [onResize]); + + useEffect(() => { + const el = ref.current; + if (!el) { + return; + } + + // Nothing to do here if we are not tracking anything. + if (!options.trackHeight && !options.trackWidth) { + return; + } + + // Before registering the observer, grab the stored size from state - we may need to restore the size. + const storedSize = selectUiSlice(getState()).textAreaSizes[id]; + + // Prefer to restore the stored size, falling back to initial size if it exists + if (storedSize?.width !== undefined) { + el.style.width = `${storedSize.width}px`; + } else if (options.initialWidth !== undefined) { + el.style.width = `${options.initialWidth}px`; + } + + if (storedSize?.height !== undefined) { + el.style.height = `${storedSize.height}px`; + } else if (options.initialHeight !== undefined) { + el.style.height = `${options.initialHeight}px`; + } + + let currentHeight = el.offsetHeight; + let currentWidth = el.offsetWidth; + + const resizeObserver = new ResizeObserver(() => { + // We only want to push the changes if a tracked dimension changes + let didChange = false; + const newSize: Partial = {}; + + if (options.trackHeight) { + if (el.offsetHeight !== currentHeight) { + didChange = true; + currentHeight = el.offsetHeight; + } + newSize.height = currentHeight; + } + + if (options.trackWidth) { + if (el.offsetWidth !== currentWidth) { + didChange = true; + currentWidth = el.offsetWidth; + } + newSize.width = currentWidth; + } + + if (didChange) { + debouncedOnResize(newSize); + } + }); + + resizeObserver.observe(el); + + return () => { + debouncedOnResize.cancel(); + resizeObserver.disconnect(); + }; + }, [ + debouncedOnResize, + dispatch, + getState, + id, + options.initialHeight, + options.initialWidth, + options.trackHeight, + options.trackWidth, + ref, + ]); +}; diff --git a/invokeai/frontend/web/src/common/hooks/useSubMenu.tsx b/invokeai/frontend/web/src/common/hooks/useSubMenu.tsx new file mode 100644 index 00000000000..4c1bc56e495 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useSubMenu.tsx @@ -0,0 +1,168 @@ +import type { MenuButtonProps, MenuItemProps, MenuListProps, MenuProps } from '@invoke-ai/ui-library'; +import { Box, Flex, Icon, Text } from '@invoke-ai/ui-library'; +import { useDisclosure } from 'common/hooks/useBoolean'; +import type { FocusEventHandler, PointerEvent, RefObject } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; +import { PiCaretRightBold } from 'react-icons/pi'; +import { useDebouncedCallback } from 'use-debounce'; + +const offset: [number, number] = [0, 8]; + +type UseSubMenuReturn = { + parentMenuItemProps: Partial; + menuProps: Partial; + menuButtonProps: Partial; + menuListProps: Partial & { ref: RefObject }; +}; + +/** + * A hook that provides the necessary props to create a sub-menu within a menu. + * + * The sub-menu should be wrapped inside a parent `MenuItem` component. + * + * Use SubMenuButtonContent to render a button with a label and a right caret icon. + * + * TODO(psyche): Add keyboard handling for sub-menu. + * + * @example + * ```tsx + * const SubMenuExample = () => { + * const subMenu = useSubMenu(); + * return ( + * + * Open Parent Menu + * + * Parent Item 1 + * Parent Item 2 + * Parent Item 3 + * }> + * + * + * + * + * + * Sub Item 1 + * Sub Item 2 + * Sub Item 3 + * + * + * + * + * + * ); + * }; + * ``` + */ +export const useSubMenu = (): UseSubMenuReturn => { + const subMenu = useDisclosure(false); + const menuListRef = useRef(null); + const closeDebounced = useDebouncedCallback(subMenu.close, 300); + const openAndCancelPendingClose = useCallback(() => { + closeDebounced.cancel(); + subMenu.open(); + }, [closeDebounced, subMenu]); + const toggleAndCancelPendingClose = useCallback(() => { + if (subMenu.isOpen) { + subMenu.close(); + return; + } else { + closeDebounced.cancel(); + subMenu.toggle(); + } + }, [closeDebounced, subMenu]); + const onBlurMenuList = useCallback>( + (e) => { + // Don't trigger blur if focus is moving to a child element - e.g. from a sub-menu item to another sub-menu item + if (e.currentTarget.contains(e.relatedTarget)) { + closeDebounced.cancel(); + return; + } + subMenu.close(); + }, + [closeDebounced, subMenu] + ); + + const onParentMenuItemPointerLeave = useCallback( + (e: PointerEvent) => { + /** + * The pointerleave event is triggered when the pen or touch device is lifted, which would close the sub-menu. + * However, we want to keep the sub-menu open until the pen or touch device pressed some other element. This + * will be handled in the useEffect below - just ignore the pointerleave event for pen and touch devices. + */ + if (e.pointerType === 'pen' || e.pointerType === 'touch') { + return; + } + subMenu.close(); + }, + [subMenu] + ); + + /** + * When using a mouse, the pointerleave events close the menu. But when using a pen or touch device, we need to close + * the sub-menu when the user taps outside of the menu list. So we need to listen for clicks outside of the menu list + * and close the menu accordingly. + */ + useEffect(() => { + const el = menuListRef.current; + if (!el) { + return; + } + const controller = new AbortController(); + window.addEventListener( + 'click', + (e) => { + if (menuListRef.current?.contains(e.target as Node)) { + return; + } + subMenu.close(); + }, + { signal: controller.signal } + ); + return () => { + controller.abort(); + }; + }, [subMenu]); + + return { + parentMenuItemProps: { + onClick: toggleAndCancelPendingClose, + onPointerEnter: openAndCancelPendingClose, + onPointerLeave: onParentMenuItemPointerLeave, + closeOnSelect: false, + }, + menuProps: { + isOpen: subMenu.isOpen, + onClose: subMenu.close, + placement: 'right', + offset: offset, + closeOnBlur: false, + }, + menuButtonProps: { + as: Box, + width: 'full', + height: 'full', + }, + menuListProps: { + ref: menuListRef, + onPointerEnter: openAndCancelPendingClose, + onPointerLeave: closeDebounced, + onBlur: onBlurMenuList, + }, + }; +}; + +export const SubMenuButtonContent = ({ label, value }: { label: string; value?: string }) => { + return ( + + {label} + + {value !== undefined && ( + + {value} + + )} + + + + ); +}; diff --git a/invokeai/frontend/web/src/common/hooks/useTimeoutCallback.ts b/invokeai/frontend/web/src/common/hooks/useTimeoutCallback.ts new file mode 100644 index 00000000000..0406d3eae55 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useTimeoutCallback.ts @@ -0,0 +1,21 @@ +import { useCallback, useMemo, useRef } from 'react'; + +export const useTimeoutCallback = (callback: () => void, delay: number, onCancel?: () => void) => { + const timeoutRef = useRef(null); + const cancel = useCallback(() => { + if (timeoutRef.current !== null) { + window.clearTimeout(timeoutRef.current); + timeoutRef.current = null; + onCancel?.(); + } + }, [onCancel]); + const callWithTimeout = useCallback(() => { + cancel(); + timeoutRef.current = window.setTimeout(() => { + callback(); + timeoutRef.current = null; + }, delay); + }, [callback, cancel, delay]); + const api = useMemo(() => [callWithTimeout, cancel] as const, [callWithTimeout, cancel]); + return api; +}; diff --git a/invokeai/frontend/web/src/common/hooks/useTouchDeviceClass.ts b/invokeai/frontend/web/src/common/hooks/useTouchDeviceClass.ts new file mode 100644 index 00000000000..574d9ac4693 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useTouchDeviceClass.ts @@ -0,0 +1,23 @@ +import { useEffect } from 'react'; + +const TOUCH_DEVICE_CLASS = 'invokeai-touch-device'; + +export const useTouchDeviceClass = () => { + useEffect(() => { + const onPointerInput = (e: PointerEvent) => { + if (e.pointerType === 'touch') { + document.documentElement.classList.add(TOUCH_DEVICE_CLASS); + } else if (e.pointerType === 'mouse') { + document.documentElement.classList.remove(TOUCH_DEVICE_CLASS); + } + }; + + window.addEventListener('pointerdown', onPointerInput, { passive: true }); + window.addEventListener('pointermove', onPointerInput, { passive: true }); + + return () => { + window.removeEventListener('pointerdown', onPointerInput); + window.removeEventListener('pointermove', onPointerInput); + }; + }, []); +}; diff --git a/invokeai/frontend/web/src/common/util/SyncableMap/SyncableMap.test.ts b/invokeai/frontend/web/src/common/util/SyncableMap/SyncableMap.test.ts new file mode 100644 index 00000000000..e65e8bf5a27 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/SyncableMap/SyncableMap.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { SyncableMap } from './SyncableMap'; + +describe('SyncableMap', () => { + it('should initialize with entries', () => { + const initialEntries = [ + ['key1', 'value1'], + ['key2', 'value2'], + ] as const; + const map = new SyncableMap(initialEntries); + expect(map.size).toBe(2); + expect(map.get('key1')).toBe('value1'); + expect(map.get('key2')).toBe('value2'); + }); + + it('should notify subscribers when a key is set', () => { + const map = new SyncableMap(); + const subscriber = vi.fn(); + map.subscribe(subscriber); + + map.set('key1', 'value1'); + + expect(subscriber).toHaveBeenCalledTimes(1); + expect(map.get('key1')).toBe('value1'); + }); + + it('should notify subscribers when a key is deleted', () => { + const map = new SyncableMap([['key1', 'value1']]); + const subscriber = vi.fn(); + map.subscribe(subscriber); + + map.delete('key1'); + + expect(subscriber).toHaveBeenCalledTimes(1); + expect(map.get('key1')).toBeUndefined(); + }); + + it('should notify subscribers when the map is cleared', () => { + const map = new SyncableMap([ + ['key1', 'value1'], + ['key2', 'value2'], + ]); + const subscriber = vi.fn(); + map.subscribe(subscriber); + + map.clear(); + + expect(subscriber).toHaveBeenCalledTimes(1); + expect(map.size).toBe(0); + }); + + it('should not notify unsubscribed callbacks', () => { + const map = new SyncableMap(); + const subscriber = vi.fn(); + const unsubscribe = map.subscribe(subscriber); + + unsubscribe(); + + map.set('key1', 'value1'); + + expect(subscriber).not.toHaveBeenCalled(); + }); + + it('should return a snapshot of the current state', () => { + const map = new SyncableMap([['key1', 'value1']]); + + const snapshot = map.getSnapshot(); + + expect(snapshot.size).toBe(1); + expect(snapshot.get('key1')).toBe('value1'); + }); + + it('should return the same snapshot if there were no changes', () => { + const map = new SyncableMap([['key1', 'value1']]); + + const firstSnapshot = map.getSnapshot(); + const secondSnapshot = map.getSnapshot(); + + expect(firstSnapshot).toBe(secondSnapshot); + }); + + it('should return a new snapshot if changes were made', () => { + const map = new SyncableMap([['key1', 'value1']]); + + const firstSnapshot = map.getSnapshot(); + map.set('key2', 'value2'); + const secondSnapshot = map.getSnapshot(); + + expect(firstSnapshot).not.toBe(secondSnapshot); + expect(secondSnapshot.size).toBe(2); + }); + + it('should consider different snapshots unequal', () => { + const map = new SyncableMap([['key1', 'value1']]); + + const firstSnapshot = map.getSnapshot(); + map.set('key2', 'value2'); + const secondSnapshot = map.getSnapshot(); + + expect(map['areSnapshotsEqual'](firstSnapshot, secondSnapshot)).toBe(false); + }); + + it('should consider identical snapshots equal', () => { + const map = new SyncableMap([ + ['key1', 'value1'], + ['key2', 'value2'], + ]); + + const firstSnapshot = map.getSnapshot(); + const secondSnapshot = map.getSnapshot(); + + expect(map['areSnapshotsEqual'](firstSnapshot, secondSnapshot)).toBe(true); + }); +}); diff --git a/invokeai/frontend/web/src/common/util/SyncableMap/SyncableMap.ts b/invokeai/frontend/web/src/common/util/SyncableMap/SyncableMap.ts new file mode 100644 index 00000000000..5743ce5ece3 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/SyncableMap/SyncableMap.ts @@ -0,0 +1,86 @@ +/** + * A Map that allows for subscribing to changes and getting a snapshot of the current state. + * + * It can be used with the `useSyncExternalStore` hook to sync the state of the map with a React component. + * + * Reactivity is shallow, so changes to nested objects will not trigger a re-render. + */ +export class SyncableMap extends Map { + private subscriptions = new Set<() => void>(); + private lastSnapshot: Map | null = null; + + constructor(entries?: readonly (readonly [K, V])[] | null) { + super(entries); + } + + set = (key: K, value: V): this => { + super.set(key, value); + this.notifySubscribers(); + return this; + }; + + delete = (key: K): boolean => { + const result = super.delete(key); + this.notifySubscribers(); + return result; + }; + + clear = (): void => { + super.clear(); + this.notifySubscribers(); + }; + + /** + * Notify all subscribers that the map has changed. + */ + private notifySubscribers = () => { + for (const callback of this.subscriptions) { + callback(); + } + }; + + /** + * Subscribe to changes to the map. + * @param callback A function to call when the map changes + * @returns A function to unsubscribe from changes + */ + subscribe = (callback: () => void): (() => void) => { + this.subscriptions.add(callback); + return () => { + this.subscriptions.delete(callback); + }; + }; + + /** + * Get a snapshot of the current state of the map. + * @returns A snapshot of the current state of the map + */ + getSnapshot = (): Map => { + const currentSnapshot = new Map(this); + if (!this.lastSnapshot || !this.areSnapshotsEqual(this.lastSnapshot, currentSnapshot)) { + this.lastSnapshot = currentSnapshot; + } + + return this.lastSnapshot; + }; + + /** + * Compare two snapshots to determine if they are equal. + * @param snapshotA The first snapshot to compare + * @param snapshotB The second snapshot to compare + * @returns Whether the two snapshots are equal + */ + private areSnapshotsEqual = (snapshotA: Map, snapshotB: Map): boolean => { + if (snapshotA.size !== snapshotB.size) { + return false; + } + + for (const [key, value] of snapshotA) { + if (!Object.is(value, snapshotB.get(key))) { + return false; + } + } + + return true; + }; +} diff --git a/invokeai/frontend/web/src/common/util/arrayUtils.test.ts b/invokeai/frontend/web/src/common/util/arrayUtils.test.ts new file mode 100644 index 00000000000..e1922fdbbeb --- /dev/null +++ b/invokeai/frontend/web/src/common/util/arrayUtils.test.ts @@ -0,0 +1,170 @@ +import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; +import { describe, expect, it } from 'vitest'; + +describe('Array Manipulation Functions', () => { + const originalArray = ['a', 'b', 'c', 'd']; + + describe('moveOneToEnd', () => { + describe('with callback', () => { + it('should move an item forward by one position', () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'b'); + expect(result).toEqual(['a', 'c', 'b', 'd']); + }); + + it('should do nothing if the item is at the end', () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'd'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'z'); + expect(result).toEqual(originalArray); + }); + }); + describe('with item', () => { + it('should move an item forward by one position', () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'b'); + expect(result).toEqual(['a', 'c', 'b', 'd']); + }); + + it('should do nothing if the item is at the end', () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'd'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'z'); + expect(result).toEqual(originalArray); + }); + }); + }); + + describe('moveToStart', () => { + describe('with callback', () => { + it('should move an item to the front', () => { + const array = [...originalArray]; + const result = moveToStart(array, (item) => item === 'c'); + expect(result).toEqual(['c', 'a', 'b', 'd']); + }); + + it('should do nothing if the item is already at the front', () => { + const array = [...originalArray]; + const result = moveToStart(array, (item) => item === 'a'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveToStart(array, (item) => item === 'z'); + expect(result).toEqual(originalArray); + }); + }); + describe('with item', () => { + it('should move an item to the front', () => { + const array = [...originalArray]; + const result = moveToStart(array, 'c'); + expect(result).toEqual(['c', 'a', 'b', 'd']); + }); + + it('should do nothing if the item is already at the front', () => { + const array = [...originalArray]; + const result = moveToStart(array, 'a'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveToStart(array, 'z'); + expect(result).toEqual(originalArray); + }); + }); + }); + + describe('moveOneToStart', () => { + describe('with callback', () => { + it('should move an item backward by one position', () => { + const array = [...originalArray]; + const result = moveOneToStart(array, (item) => item === 'c'); + expect(result).toEqual(['a', 'c', 'b', 'd']); + }); + + it('should do nothing if the item is at the beginning', () => { + const array = [...originalArray]; + const result = moveOneToStart(array, (item) => item === 'a'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveOneToStart(array, (item) => item === 'z'); + expect(result).toEqual(originalArray); + }); + }); + describe('with item', () => { + it('should move an item backward by one position', () => { + const array = [...originalArray]; + const result = moveOneToStart(array, 'c'); + expect(result).toEqual(['a', 'c', 'b', 'd']); + }); + + it('should do nothing if the item is at the beginning', () => { + const array = [...originalArray]; + const result = moveOneToStart(array, 'a'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveOneToStart(array, 'z'); + expect(result).toEqual(originalArray); + }); + }); + }); + + describe('moveToEnd', () => { + describe('with callback', () => { + it('should move an item to the back', () => { + const array = [...originalArray]; + const result = moveToEnd(array, (item) => item === 'b'); + expect(result).toEqual(['a', 'c', 'd', 'b']); + }); + + it('should do nothing if the item is already at the back', () => { + const array = [...originalArray]; + const result = moveToEnd(array, (item) => item === 'd'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveToEnd(array, (item) => item === 'z'); + expect(result).toEqual(originalArray); + }); + }); + describe('with item', () => { + it('should move an item to the back', () => { + const array = [...originalArray]; + const result = moveToEnd(array, 'b'); + expect(result).toEqual(['a', 'c', 'd', 'b']); + }); + + it('should do nothing if the item is already at the back', () => { + const array = [...originalArray]; + const result = moveToEnd(array, 'd'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveToEnd(array, 'z'); + expect(result).toEqual(originalArray); + }); + }); + }); +}); diff --git a/invokeai/frontend/web/src/common/util/arrayUtils.ts b/invokeai/frontend/web/src/common/util/arrayUtils.ts new file mode 100644 index 00000000000..9f0d4cfbf6c --- /dev/null +++ b/invokeai/frontend/web/src/common/util/arrayUtils.ts @@ -0,0 +1,45 @@ +export function moveToStart(array: T[], selectItemCallback: (item: T) => boolean): T[]; +export function moveToStart(array: T[], item: T): T[]; +export function moveToStart(array: T[], arg1: T | ((item: T) => boolean)): T[] { + const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1); + if (index > 0) { + const [item] = array.splice(index, 1); + //@ts-expect-error - These indicies are safe per the previous check + array.unshift(item); + } + return array; +} + +export function moveOneToStart(array: T[], selectItemCallback: (item: T) => boolean): T[]; +export function moveOneToStart(array: T[], item: T): T[]; +export function moveOneToStart(array: T[], arg1: T | ((item: T) => boolean)): T[] { + const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1); + if (index > 0) { + //@ts-expect-error - These indicies are safe per the previous check + [array[index], array[index - 1]] = [array[index - 1], array[index]]; + } + return array; +} + +export function moveToEnd(array: T[], selectItemCallback: (item: T) => boolean): T[]; +export function moveToEnd(array: T[], item: T): T[]; +export function moveToEnd(array: T[], arg1: T | ((item: T) => boolean)): T[] { + const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1); + if (index >= 0 && index < array.length - 1) { + const [item] = array.splice(index, 1); + //@ts-expect-error - These indicies are safe per the previous check + array.push(item); + } + return array; +} + +export function moveOneToEnd(array: T[], selectItemCallback: (item: T) => boolean): T[]; +export function moveOneToEnd(array: T[], item: T): T[]; +export function moveOneToEnd(array: T[], arg1: T | ((item: T) => boolean)): T[] { + const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1); + if (index >= 0 && index < array.length - 1) { + //@ts-expect-error - These indicies are safe per the previous check + [array[index], array[index + 1]] = [array[index + 1], array[index]]; + } + return array; +} diff --git a/invokeai/frontend/web/src/common/util/colorCodeTransformers.ts b/invokeai/frontend/web/src/common/util/colorCodeTransformers.ts new file mode 100644 index 00000000000..85635f93b55 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/colorCodeTransformers.ts @@ -0,0 +1,27 @@ +import type { RgbaColor, RgbColor } from 'react-colorful'; + +export function rgbaToHex(color: RgbaColor, alpha: boolean = false): string { + const hex = ((1 << 24) + (color.r << 16) + (color.g << 8) + color.b).toString(16).slice(1); + const alphaHex = Math.round(color.a * 255) + .toString(16) + .padStart(2, '0'); + return alpha ? `#${hex}${alphaHex}` : `#${hex}`; +} + +export function hexToRGBA(hex: string, alpha: number) { + hex = hex.replace(/^#/, ''); + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + return { r, g, b, a: alpha }; +} + +export const rgbaColorToString = (color: RgbaColor): string => { + const { r, g, b, a } = color; + return `rgba(${r}, ${g}, ${b}, ${a})`; +}; + +export const rgbColorToString = (color: RgbColor): string => { + const { r, g, b } = color; + return `rgba(${r}, ${g}, ${b})`; +}; diff --git a/invokeai/frontend/web/src/common/util/colorTokenToCssVar.ts b/invokeai/frontend/web/src/common/util/colorTokenToCssVar.ts new file mode 100644 index 00000000000..0e7cce3cdf5 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/colorTokenToCssVar.ts @@ -0,0 +1 @@ +export const colorTokenToCssVar = (colorToken: string) => `var(--invoke-colors-${colorToken.split('.').join('-')})`; diff --git a/invokeai/frontend/web/src/common/util/convertImageUrlToBlob.ts b/invokeai/frontend/web/src/common/util/convertImageUrlToBlob.ts new file mode 100644 index 00000000000..69816bd0284 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/convertImageUrlToBlob.ts @@ -0,0 +1,43 @@ +/** + * Converts an image URL to a Blob by creating an element, drawing it to canvas + * and then converting the canvas to a Blob. + * + * @returns A function that takes a URL and returns a Promise that resolves with a Blob + */ + +export const convertImageUrlToBlob = (url: string) => + new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + if (img.width === 0 || img.height === 0) { + reject(new Error('Image has no dimensions. The URL may be invalid or the object may not exist.')); + return; + } + + const canvas = document.createElement('canvas'); + + canvas.width = img.width; + canvas.height = img.height; + + const context = canvas.getContext('2d'); + if (!context) { + reject(new Error('Failed to get canvas context')); + return; + } + context.drawImage(img, 0, 0); + canvas.toBlob((blob) => { + if (blob) { + resolve(blob); + } else { + reject(new Error('Failed to convert image to blob')); + } + }, 'image/png'); + }; + + img.onerror = () => { + reject(new Error('Image failed to load. The URL may be invalid or the object may not exist.')); + }; + + img.crossOrigin = 'anonymous'; + img.src = url; + }); diff --git a/invokeai/frontend/web/src/common/util/createDeferredPromise.ts b/invokeai/frontend/web/src/common/util/createDeferredPromise.ts new file mode 100644 index 00000000000..b82bb795cc4 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/createDeferredPromise.ts @@ -0,0 +1,20 @@ +export type Deferred = { + promise: Promise; + resolve: (value: T) => void; + reject: (error: Error) => void; +}; + +/** + * Create a promise and expose its resolve and reject callbacks. + */ +export const createDeferredPromise = (): Deferred => { + let resolve!: (value: T) => void; + let reject!: (error: Error) => void; + + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + return { promise, resolve, reject }; +}; diff --git a/invokeai/frontend/web/src/common/util/deepClone.ts b/invokeai/frontend/web/src/common/util/deepClone.ts new file mode 100644 index 00000000000..211fc6c5b47 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/deepClone.ts @@ -0,0 +1,15 @@ +import rfdc from 'rfdc'; +const _rfdc = rfdc(); + +/** + * Deep-clones an object using Really Fast Deep Clone. + * This is the fastest deep clone library on Chrome, but not the fastest on FF. Still, it's much faster than lodash + * and structuredClone, so it's the best all-around choice. + * + * Simple Benchmark: https://www.measurethat.net/Benchmarks/Show/30358/0/lodash-clonedeep-vs-jsonparsejsonstringify-vs-recursive + * Repo: https://github.com/davidmarkclements/rfdc + * + * @param obj The object to deep-clone + * @returns The cloned object + */ +export const deepClone = (obj: T): T => _rfdc(obj); diff --git a/invokeai/frontend/web/src/common/util/extractMessageFromAssertionError.ts b/invokeai/frontend/web/src/common/util/extractMessageFromAssertionError.ts new file mode 100644 index 00000000000..a6c61bb6f30 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/extractMessageFromAssertionError.ts @@ -0,0 +1,6 @@ +import type { AssertionError } from 'tsafe'; + +export function extractMessageFromAssertionError(error: AssertionError): string | null { + const match = error.message.match(/Wrong assertion encountered: "(.*)"/); + return match ? (match[1] ?? null) : null; +} diff --git a/invokeai/frontend/web/src/common/util/fixTooltipCloseOnScrollStyles.ts b/invokeai/frontend/web/src/common/util/fixTooltipCloseOnScrollStyles.ts new file mode 100644 index 00000000000..1cb879e0b16 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/fixTooltipCloseOnScrollStyles.ts @@ -0,0 +1,15 @@ +import type { CSSProperties } from 'react'; + +/** + * Chakra's Tooltip's method of finding the nearest scroll parent has a problem - it assumes the first parent with + * `overflow: hidden` is the scroll parent. In this case, the Collapse component has that style, but isn't scrollable + * itself. The result is that the tooltip does not close on scroll, because the scrolling happens higher up in the DOM. + * + * As a hacky workaround, we can set the overflow to `visible`, which allows the scroll parent search to continue up to + * the actual scroll parent (in this case, the OverlayScrollbarsComponent in BoardsListWrapper). + * + * See: https://github.com/chakra-ui/chakra-ui/issues/7871#issuecomment-2453780958 + */ +export const fixTooltipCloseOnScrollStyles: CSSProperties = { + overflow: 'visible', +}; diff --git a/invokeai/frontend/web/src/common/util/generateSeeds.ts b/invokeai/frontend/web/src/common/util/generateSeeds.ts new file mode 100644 index 00000000000..06faf2cf26e --- /dev/null +++ b/invokeai/frontend/web/src/common/util/generateSeeds.ts @@ -0,0 +1,18 @@ +import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants'; +import { random } from 'es-toolkit/compat'; + +type GenerateSeedsArg = { + count: number; + start?: number; + min?: number; + max?: number; +}; + +export const generateSeeds = ({ count, start, min = NUMPY_RAND_MIN, max = NUMPY_RAND_MAX }: GenerateSeedsArg) => { + const first = start ?? random(min, max); + const seeds: number[] = []; + for (let i = first; i < first + count; i++) { + seeds.push(i % max); + } + return seeds; +}; diff --git a/invokeai/frontend/web/src/common/util/openImageInNewTab.ts b/invokeai/frontend/web/src/common/util/openImageInNewTab.ts new file mode 100644 index 00000000000..3e8e13334c2 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/openImageInNewTab.ts @@ -0,0 +1,3 @@ +export const openImageInNewTab = (imageUrl: string) => { + window.open(imageUrl, '_blank', 'noopener,noreferrer'); +}; diff --git a/invokeai/frontend/web/src/common/util/promptAST.test.ts b/invokeai/frontend/web/src/common/util/promptAST.test.ts new file mode 100644 index 00000000000..32ad9dc09fb --- /dev/null +++ b/invokeai/frontend/web/src/common/util/promptAST.test.ts @@ -0,0 +1,778 @@ +import { describe, expect, it } from 'vitest'; + +import { parseTokens, serialize, tokenize } from './promptAST'; + +describe('promptAST', () => { + describe('tokenize', () => { + it('should tokenize basic text', () => { + const tokens = tokenize('a cat'); + expect(tokens).toEqual([ + { type: 'word', value: 'a', start: 0, end: 1 }, + { type: 'whitespace', value: ' ', start: 1, end: 2 }, + { type: 'word', value: 'cat', start: 2, end: 5 }, + ]); + }); + + it('should tokenize groups with parentheses', () => { + const tokens = tokenize('(a cat)'); + expect(tokens).toEqual([ + { type: 'lparen', start: 0, end: 1 }, + { type: 'word', value: 'a', start: 1, end: 2 }, + { type: 'whitespace', value: ' ', start: 2, end: 3 }, + { type: 'word', value: 'cat', start: 3, end: 6 }, + { type: 'rparen', start: 6, end: 7 }, + ]); + }); + + it('should tokenize escaped parentheses', () => { + const tokens = tokenize('\\(medium\\)'); + expect(tokens).toEqual([ + { type: 'escaped_paren', value: '(', start: 0, end: 2 }, + { type: 'word', value: 'medium', start: 2, end: 8 }, + { type: 'escaped_paren', value: ')', start: 8, end: 10 }, + ]); + }); + + it('should tokenize mixed escaped and unescaped parentheses', () => { + const tokens = tokenize('colored pencil \\(medium\\) (enhanced)'); + expect(tokens).toEqual([ + { type: 'word', value: 'colored', start: 0, end: 7 }, + { type: 'whitespace', value: ' ', start: 7, end: 8 }, + { type: 'word', value: 'pencil', start: 8, end: 14 }, + { type: 'whitespace', value: ' ', start: 14, end: 15 }, + { type: 'escaped_paren', value: '(', start: 15, end: 17 }, + { type: 'word', value: 'medium', start: 17, end: 23 }, + { type: 'escaped_paren', value: ')', start: 23, end: 25 }, + { type: 'whitespace', value: ' ', start: 25, end: 26 }, + { type: 'lparen', start: 26, end: 27 }, + { type: 'word', value: 'enhanced', start: 27, end: 35 }, + { type: 'rparen', start: 35, end: 36 }, + ]); + }); + + it('should tokenize groups with weights', () => { + const tokens = tokenize('(a cat)1.2'); + expect(tokens).toEqual([ + { type: 'lparen', start: 0, end: 1 }, + { type: 'word', value: 'a', start: 1, end: 2 }, + { type: 'whitespace', value: ' ', start: 2, end: 3 }, + { type: 'word', value: 'cat', start: 3, end: 6 }, + { type: 'rparen', start: 6, end: 7 }, + { type: 'weight', value: 1.2, start: 7, end: 10 }, + ]); + }); + + it('should tokenize words with weights', () => { + const tokens = tokenize('cat+'); + expect(tokens).toEqual([ + { type: 'word', value: 'cat', start: 0, end: 3 }, + { type: 'weight', value: '+', start: 3, end: 4 }, + ]); + }); + + it('should tokenize embeddings', () => { + const tokens = tokenize(''); + expect(tokens).toEqual([ + { type: 'lembed', start: 0, end: 1 }, + { type: 'word', value: 'embedding_name', start: 1, end: 15 }, + { type: 'rembed', start: 15, end: 16 }, + ]); + }); + + it('should tokenize prompt function syntax', () => { + const tokens = tokenize("('a', 'b').and()"); + expect(tokens).toEqual([ + { type: 'lparen', start: 0, end: 1 }, + { type: 'punct', value: "'", start: 1, end: 2 }, + { type: 'word', value: 'a', start: 2, end: 3 }, + { type: 'punct', value: "'", start: 3, end: 4 }, + { type: 'punct', value: ',', start: 4, end: 5 }, + { type: 'whitespace', value: ' ', start: 5, end: 6 }, + { type: 'punct', value: "'", start: 6, end: 7 }, + { type: 'word', value: 'b', start: 7, end: 8 }, + { type: 'punct', value: "'", start: 8, end: 9 }, + { type: 'rparen', start: 9, end: 10 }, + { type: 'punct', value: '.', start: 10, end: 11 }, + { type: 'word', value: 'and', start: 11, end: 14 }, + { type: 'lparen', start: 14, end: 15 }, + { type: 'rparen', start: 15, end: 16 }, + ]); + }); + + it('should tokenize curly/smart quotes as punctuation', () => { + const tokens = tokenize('\u201chello\u201d'); + expect(tokens).toEqual([ + { type: 'punct', value: '\u201c', start: 0, end: 1 }, + { type: 'word', value: 'hello', start: 1, end: 6 }, + { type: 'punct', value: '\u201d', start: 6, end: 7 }, + ]); + }); + + it('should tokenize curly single quotes as punctuation', () => { + const tokens = tokenize('\u2018hello\u2019'); + expect(tokens).toEqual([ + { type: 'punct', value: '\u2018', start: 0, end: 1 }, + { type: 'word', value: 'hello', start: 1, end: 6 }, + { type: 'punct', value: '\u2019', start: 6, end: 7 }, + ]); + }); + }); + + describe('parseTokens', () => { + it('should parse basic text', () => { + const tokens = tokenize('a cat'); + const ast = parseTokens(tokens); + expect(ast).toEqual([ + { type: 'word', text: 'a', range: { start: 0, end: 1 }, attention: undefined }, + { type: 'whitespace', value: ' ', range: { start: 1, end: 2 } }, + { type: 'word', text: 'cat', range: { start: 2, end: 5 }, attention: undefined }, + ]); + }); + + it('should parse groups', () => { + const tokens = tokenize('(a cat)'); + const ast = parseTokens(tokens); + expect(ast).toEqual([ + { + type: 'group', + range: { start: 0, end: 7 }, + attention: undefined, + children: [ + { type: 'word', text: 'a', range: { start: 1, end: 2 }, attention: undefined }, + { type: 'whitespace', value: ' ', range: { start: 2, end: 3 } }, + { type: 'word', text: 'cat', range: { start: 3, end: 6 }, attention: undefined }, + ], + }, + ]); + }); + + it('should parse escaped parentheses', () => { + const tokens = tokenize('\\(medium\\)'); + const ast = parseTokens(tokens); + expect(ast).toEqual([ + { type: 'escaped_paren', value: '(', range: { start: 0, end: 2 } }, + { type: 'word', text: 'medium', range: { start: 2, end: 8 }, attention: undefined }, + { type: 'escaped_paren', value: ')', range: { start: 8, end: 10 } }, + ]); + }); + + it('should parse mixed escaped and unescaped parentheses', () => { + const tokens = tokenize('colored pencil \\(medium\\) (enhanced)'); + const ast = parseTokens(tokens); + expect(ast).toEqual([ + { type: 'word', text: 'colored', range: { start: 0, end: 7 }, attention: undefined }, + { type: 'whitespace', value: ' ', range: { start: 7, end: 8 } }, + { type: 'word', text: 'pencil', range: { start: 8, end: 14 }, attention: undefined }, + { type: 'whitespace', value: ' ', range: { start: 14, end: 15 } }, + { type: 'escaped_paren', value: '(', range: { start: 15, end: 17 } }, + { type: 'word', text: 'medium', range: { start: 17, end: 23 }, attention: undefined }, + { type: 'escaped_paren', value: ')', range: { start: 23, end: 25 } }, + { type: 'whitespace', value: ' ', range: { start: 25, end: 26 } }, + { + type: 'group', + range: { start: 26, end: 36 }, + attention: undefined, + children: [{ type: 'word', text: 'enhanced', range: { start: 27, end: 35 }, attention: undefined }], + }, + ]); + }); + + it('should parse groups with attention', () => { + const tokens = tokenize('(a cat)1.2'); + const ast = parseTokens(tokens); + expect(ast).toEqual([ + { + type: 'group', + attention: 1.2, + range: { start: 0, end: 10 }, + children: [ + { type: 'word', text: 'a', range: { start: 1, end: 2 }, attention: undefined }, + { type: 'whitespace', value: ' ', range: { start: 2, end: 3 } }, + { type: 'word', text: 'cat', range: { start: 3, end: 6 }, attention: undefined }, + ], + }, + ]); + }); + + it('should parse words with attention', () => { + const tokens = tokenize('cat+'); + const ast = parseTokens(tokens); + expect(ast).toEqual([{ type: 'word', text: 'cat', attention: '+', range: { start: 0, end: 4 } }]); + }); + + it('should parse embeddings', () => { + const tokens = tokenize(''); + const ast = parseTokens(tokens); + expect(ast).toEqual([{ type: 'embedding', value: 'embedding_name', range: { start: 0, end: 16 } }]); + }); + + describe('prompt functions', () => { + it('should parse .and() prompt function with single-quoted args', () => { + const tokens = tokenize("('one two', 'three four').and()"); + const ast = parseTokens(tokens); + expect(ast).toHaveLength(1); + + const pf = ast[0]!; + expect(pf.type).toBe('prompt_function'); + if (pf.type !== 'prompt_function') { + return; + } + expect(pf.name).toBe('and'); + expect(pf.functionParams).toBe(''); + expect(pf.promptArgs).toHaveLength(2); + + // First arg: 'one two' + expect(pf.promptArgs[0]!.quote).toBe("'"); + expect(pf.promptArgs[0]!.nodes).toHaveLength(3); // word, ws, word + expect(pf.promptArgs[0]!.nodes[0]).toMatchObject({ type: 'word', text: 'one' }); + expect(pf.promptArgs[0]!.nodes[2]).toMatchObject({ type: 'word', text: 'two' }); + + // Second arg: 'three four' + expect(pf.promptArgs[1]!.quote).toBe("'"); + expect(pf.promptArgs[1]!.nodes).toHaveLength(3); + expect(pf.promptArgs[1]!.nodes[0]).toMatchObject({ type: 'word', text: 'three' }); + expect(pf.promptArgs[1]!.nodes[2]).toMatchObject({ type: 'word', text: 'four' }); + }); + + it('should parse .or() prompt function', () => { + const tokens = tokenize("('one', 'two three. four.').or()"); + const ast = parseTokens(tokens); + expect(ast).toHaveLength(1); + + const pf = ast[0]!; + expect(pf.type).toBe('prompt_function'); + if (pf.type !== 'prompt_function') { + return; + } + expect(pf.name).toBe('or'); + expect(pf.promptArgs).toHaveLength(2); + + // First arg: 'one' + expect(pf.promptArgs[0]!.nodes).toHaveLength(1); + expect(pf.promptArgs[0]!.nodes[0]).toMatchObject({ type: 'word', text: 'one' }); + + // Second arg: 'two three. four.' + expect(pf.promptArgs[1]!.nodes.length).toBeGreaterThanOrEqual(5); + }); + + it('should parse .blend() prompt function with params', () => { + const tokens = tokenize("('one', 'two').blend(0.7, 0.3)"); + const ast = parseTokens(tokens); + expect(ast).toHaveLength(1); + + const pf = ast[0]!; + expect(pf.type).toBe('prompt_function'); + if (pf.type !== 'prompt_function') { + return; + } + expect(pf.name).toBe('blend'); + expect(pf.functionParams).toBe('0.7, 0.3'); + expect(pf.promptArgs).toHaveLength(2); + }); + + it('should parse prompt function with double-quoted args', () => { + const tokens = tokenize('("one", "two").and()'); + const ast = parseTokens(tokens); + expect(ast).toHaveLength(1); + + const pf = ast[0]!; + expect(pf.type).toBe('prompt_function'); + if (pf.type !== 'prompt_function') { + return; + } + expect(pf.name).toBe('and'); + expect(pf.promptArgs[0]!.quote).toBe('"'); + }); + + it('should parse prompt function with curly double quotes', () => { + const tokens = tokenize('(\u201cone\u201d, \u201ctwo\u201d).and()'); + const ast = parseTokens(tokens); + expect(ast).toHaveLength(1); + + const pf = ast[0]!; + expect(pf.type).toBe('prompt_function'); + if (pf.type !== 'prompt_function') { + return; + } + expect(pf.name).toBe('and'); + expect(pf.promptArgs).toHaveLength(2); + expect(pf.promptArgs[0]!.quote).toBe('\u201c'); + expect(pf.promptArgs[0]!.nodes[0]).toMatchObject({ type: 'word', text: 'one' }); + expect(pf.promptArgs[1]!.nodes[0]).toMatchObject({ type: 'word', text: 'two' }); + }); + + it('should parse prompt function with curly single quotes', () => { + const tokens = tokenize('(\u2018one\u2019, \u2018two\u2019).and()'); + const ast = parseTokens(tokens); + expect(ast).toHaveLength(1); + + const pf = ast[0]!; + expect(pf.type).toBe('prompt_function'); + if (pf.type !== 'prompt_function') { + return; + } + expect(pf.name).toBe('and'); + expect(pf.promptArgs[0]!.quote).toBe('\u2018'); + }); + + it('should parse prompt function with curly quotes containing commas in args', () => { + const prompt = '(\u201chigh detail, cinematic\u201d, \u201csoft light, portrait\u201d).and()'; + const ast = parseTokens(tokenize(prompt)); + expect(ast).toHaveLength(1); + + const pf = ast[0]!; + expect(pf.type).toBe('prompt_function'); + if (pf.type !== 'prompt_function') { + return; + } + expect(pf.promptArgs).toHaveLength(2); + }); + + it('should parse prompt function with newline before .method()', () => { + const prompt = '(\u201cone\u201d, \u201ctwo\u201d)\n.and()'; + const ast = parseTokens(tokenize(prompt)); + expect(ast).toHaveLength(1); + expect(ast[0]!.type).toBe('prompt_function'); + }); + + it('should parse quoted prompt function with newline before .method()', () => { + const prompt = "('one', 'two')\n.and()"; + const ast = parseTokens(tokenize(prompt)); + expect(ast).toHaveLength(1); + expect(ast[0]!.type).toBe('prompt_function'); + }); + + it('should parse prompt function with attention inside args', () => { + const tokens = tokenize("('hello+', '(world)-').and()"); + const ast = parseTokens(tokens); + expect(ast).toHaveLength(1); + + const pf = ast[0]!; + expect(pf.type).toBe('prompt_function'); + if (pf.type !== 'prompt_function') { + return; + } + + // First arg: hello+ + const arg0Word = pf.promptArgs[0]!.nodes[0]!; + expect(arg0Word).toMatchObject({ type: 'word', text: 'hello', attention: '+' }); + + // Second arg: (world)- + const arg1Group = pf.promptArgs[1]!.nodes[0]!; + expect(arg1Group.type).toBe('group'); + if (arg1Group.type === 'group') { + expect(arg1Group.attention).toBe('-'); + } + }); + + it('should preserve content range for each arg', () => { + const tokens = tokenize("('one two', 'three four').and()"); + const ast = parseTokens(tokens); + const pf = ast[0]!; + expect(pf.type).toBe('prompt_function'); + if (pf.type !== 'prompt_function') { + return; + } + + // 'one two' content is between quotes at positions 1 and 9 + expect(pf.promptArgs[0]!.contentRange.start).toBe(2); + expect(pf.promptArgs[0]!.contentRange.end).toBe(9); + + // 'three four' content is between quotes at positions 12 and 23 + expect(pf.promptArgs[1]!.contentRange.start).toBe(13); + expect(pf.promptArgs[1]!.contentRange.end).toBe(23); + }); + + it('should parse prompt function embedded in larger prompt', () => { + const tokens = tokenize("some text, ('a', 'b').and(), more text"); + const ast = parseTokens(tokens); + + // Should have: word, ws, word, punct, ws, prompt_function, punct, ws, word, ws, word + const pfNodes = ast.filter((n) => n.type === 'prompt_function'); + expect(pfNodes).toHaveLength(1); + expect(pfNodes[0]!.type).toBe('prompt_function'); + }); + + it('should fall back to regular group when no method call follows', () => { + const tokens = tokenize("('a', 'b')"); + const ast = parseTokens(tokens); + + // Without .method(), this should be parsed as a regular group + expect(ast[0]!.type).toBe('group'); + }); + + it('should parse three-arg prompt function', () => { + const tokens = tokenize("('a', 'b', 'c').blend(0.5, 0.3, 0.2)"); + const ast = parseTokens(tokens); + expect(ast).toHaveLength(1); + + const pf = ast[0]!; + expect(pf.type).toBe('prompt_function'); + if (pf.type !== 'prompt_function') { + return; + } + expect(pf.promptArgs).toHaveLength(3); + expect(pf.functionParams).toBe('0.5, 0.3, 0.2'); + }); + }); + + describe('unquoted prompt functions', () => { + it('should parse unquoted .and() prompt function', () => { + const tokens = tokenize('(one,two).and()'); + const ast = parseTokens(tokens); + expect(ast).toHaveLength(1); + + const pf = ast[0]!; + expect(pf.type).toBe('prompt_function'); + if (pf.type !== 'prompt_function') { + return; + } + expect(pf.name).toBe('and'); + expect(pf.functionParams).toBe(''); + expect(pf.promptArgs).toHaveLength(2); + expect(pf.promptArgs[0]!.quote).toBe(''); + expect(pf.promptArgs[0]!.nodes[0]).toMatchObject({ type: 'word', text: 'one' }); + expect(pf.promptArgs[1]!.quote).toBe(''); + expect(pf.promptArgs[1]!.nodes[0]).toMatchObject({ type: 'word', text: 'two' }); + }); + + it('should parse unquoted .and() with spaces', () => { + const tokens = tokenize('(one two, three four).and()'); + const ast = parseTokens(tokens); + expect(ast).toHaveLength(1); + + const pf = ast[0]!; + expect(pf.type).toBe('prompt_function'); + if (pf.type !== 'prompt_function') { + return; + } + expect(pf.name).toBe('and'); + expect(pf.promptArgs).toHaveLength(2); + expect(pf.promptArgs[0]!.nodes[0]).toMatchObject({ type: 'word', text: 'one' }); + expect(pf.promptArgs[0]!.nodes[2]).toMatchObject({ type: 'word', text: 'two' }); + expect(pf.promptArgs[1]!.nodes[0]).toMatchObject({ type: 'word', text: 'three' }); + expect(pf.promptArgs[1]!.nodes[2]).toMatchObject({ type: 'word', text: 'four' }); + }); + + it('should parse unquoted .blend() with params', () => { + const tokens = tokenize('(one two, three four).blend(0.7, 0.3)'); + const ast = parseTokens(tokens); + expect(ast).toHaveLength(1); + + const pf = ast[0]!; + expect(pf.type).toBe('prompt_function'); + if (pf.type !== 'prompt_function') { + return; + } + expect(pf.name).toBe('blend'); + expect(pf.functionParams).toBe('0.7, 0.3'); + expect(pf.promptArgs).toHaveLength(2); + }); + + it('should parse unquoted three-arg prompt function', () => { + const tokens = tokenize('(a, b, c).blend(0.5, 0.3, 0.2)'); + const ast = parseTokens(tokens); + expect(ast).toHaveLength(1); + + const pf = ast[0]!; + expect(pf.type).toBe('prompt_function'); + if (pf.type !== 'prompt_function') { + return; + } + expect(pf.promptArgs).toHaveLength(3); + expect(pf.functionParams).toBe('0.5, 0.3, 0.2'); + }); + + it('should parse unquoted prompt function with attention inside args', () => { + const tokens = tokenize('(hello+, world).and()'); + const ast = parseTokens(tokens); + expect(ast).toHaveLength(1); + + const pf = ast[0]!; + expect(pf.type).toBe('prompt_function'); + if (pf.type !== 'prompt_function') { + return; + } + const arg0Word = pf.promptArgs[0]!.nodes[0]!; + expect(arg0Word).toMatchObject({ type: 'word', text: 'hello', attention: '+' }); + }); + + it('should fall back to regular group for single-arg unquoted function', () => { + const tokens = tokenize('(hello world).and()'); + const ast = parseTokens(tokens); + // Without a comma, this is not detected as a prompt function + expect(ast[0]!.type).toBe('group'); + }); + + it('should parse unquoted prompt function embedded in larger prompt', () => { + const tokens = tokenize('some text, (a, b).and(), more text'); + const ast = parseTokens(tokens); + const pfNodes = ast.filter((n) => n.type === 'prompt_function'); + expect(pfNodes).toHaveLength(1); + }); + }); + }); + + describe('serialize', () => { + it('should serialize basic text', () => { + const tokens = tokenize('a cat'); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe('a cat'); + }); + + it('should serialize groups', () => { + const tokens = tokenize('(a cat)'); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe('(a cat)'); + }); + + it('should serialize escaped parentheses', () => { + const tokens = tokenize('\\(medium\\)'); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe('\\(medium\\)'); + }); + + it('should serialize mixed escaped and unescaped parentheses', () => { + const tokens = tokenize('colored pencil \\(medium\\) (enhanced)'); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe('colored pencil \\(medium\\) (enhanced)'); + }); + + it('should serialize groups with attention', () => { + const tokens = tokenize('(a cat)1.2'); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe('(a cat)1.2'); + }); + + it('should serialize words with attention', () => { + const tokens = tokenize('cat+'); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe('cat+'); + }); + + it('should serialize embeddings', () => { + const tokens = tokenize(''); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe(''); + }); + + describe('prompt functions', () => { + it('should serialize .and() prompt function', () => { + const tokens = tokenize("('one two', 'three four').and()"); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe("('one two', 'three four').and()"); + }); + + it('should serialize .or() prompt function', () => { + const tokens = tokenize("('one', 'two three. four.').or()"); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe("('one', 'two three. four.').or()"); + }); + + it('should serialize .blend() with params', () => { + const tokens = tokenize("('one', 'two').blend(0.7, 0.3)"); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe("('one', 'two').blend(0.7, 0.3)"); + }); + + it('should serialize prompt function with attention inside args', () => { + const tokens = tokenize("('hello+', '(world)-').and()"); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe("('hello+', '(world)-').and()"); + }); + + it('should serialize prompt function embedded in larger prompt', () => { + const prompt = "some text, ('a', 'b').and(), more text"; + const tokens = tokenize(prompt); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe(prompt); + }); + + it('should serialize three-arg blend', () => { + const tokens = tokenize("('a', 'b', 'c').blend(0.5, 0.3, 0.2)"); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe("('a', 'b', 'c').blend(0.5, 0.3, 0.2)"); + }); + + it('should serialize double-quoted prompt function', () => { + const tokens = tokenize('("one", "two").and()'); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe('("one", "two").and()'); + }); + + it('should serialize curly double-quoted prompt function', () => { + const tokens = tokenize('(\u201cone\u201d, \u201ctwo\u201d).and()'); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe('(\u201cone\u201d, \u201ctwo\u201d).and()'); + }); + + it('should serialize curly single-quoted prompt function', () => { + const tokens = tokenize('(\u2018one\u2019, \u2018two\u2019).and()'); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe('(\u2018one\u2019, \u2018two\u2019).and()'); + }); + }); + + describe('unquoted prompt functions', () => { + it('should serialize unquoted .and()', () => { + const tokens = tokenize('(one two, three four).and()'); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe('(one two, three four).and()'); + }); + + it('should serialize unquoted .blend() with params', () => { + const tokens = tokenize('(one two, three four).blend(0.7, 0.3)'); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe('(one two, three four).blend(0.7, 0.3)'); + }); + + it('should serialize unquoted prompt function embedded in larger prompt', () => { + const prompt = 'some text, (a, b).and(), more text'; + const tokens = tokenize(prompt); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe(prompt); + }); + }); + }); + + describe('round-trip (tokenize → parse → serialize)', () => { + const roundTrip = (prompt: string) => { + const tokens = tokenize(prompt); + const ast = parseTokens(tokens); + return serialize(ast); + }; + + it.each([ + 'a cat', + '(a cat)', + '(a cat)1.2', + 'cat+', + 'cat++', + 'cat-', + '(hello world)+', + '(hello world)++', + '(hello world)-', + '\\(medium\\)', + 'colored pencil \\(medium\\) (enhanced)', + '', + 'portrait \\(realistic\\) (high quality)1.2', + '(masterpiece)1.3, best quality, (high detail)1.2', + "('one two', 'three four').and()", + "('one', 'two three. four.').or()", + "('one', 'two').blend(0.7, 0.3)", + "('hello+', '(world)-').and()", + "some text, ('a', 'b').and(), more text", + "('a', 'b', 'c').blend(0.5, 0.3, 0.2)", + '("one", "two").and()', + // Curly double-quoted prompt functions + '(\u201cone\u201d, \u201ctwo\u201d).and()', + '(\u201chigh detail, cinematic\u201d, \u201csoft light, portrait\u201d).and()', + '(\u201cone\u201d, \u201ctwo\u201d).blend(0.7, 0.3)', + // Curly single-quoted prompt functions + '(\u2018one\u2019, \u2018two\u2019).and()', + '(\u2018one\u2019, \u2018two\u2019).or()', + // Unquoted prompt functions + '(one two, three four).and()', + '(one two, three four).blend(0.7, 0.3)', + '(a, b, c).blend(0.5, 0.3, 0.2)', + 'some text, (a, b).and(), more text', + "('one',\n 'two',\n 'three').and()", + ])('should round-trip: %s', (prompt) => { + expect(roundTrip(prompt)).toBe(prompt); + }); + }); + + describe('newline normalization', () => { + const roundTrip = (prompt: string) => { + const tokens = tokenize(prompt); + const ast = parseTokens(tokens); + return serialize(ast); + }; + + it('should normalize newline before .method() in quoted prompt function', () => { + expect(roundTrip("('one', 'two')\n.and()")).toBe("('one', 'two').and()"); + }); + + it('should normalize newline before .method() in curly-quoted prompt function', () => { + expect(roundTrip('(\u201cone\u201d, \u201ctwo\u201d)\n.and()')).toBe('(\u201cone\u201d, \u201ctwo\u201d).and()'); + }); + + it('should normalize newline before .method() in unquoted prompt function', () => { + expect(roundTrip('(one, two)\n.and()')).toBe('(one, two).and()'); + }); + }); + + describe('compel compatibility examples', () => { + it('should handle escaped parentheses for literal text', () => { + const prompt = 'A bear \\(with razor-sharp teeth\\) in a forest.'; + const tokens = tokenize(prompt); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe(prompt); + }); + + it('should handle unescaped parentheses as grouping syntax', () => { + const prompt = 'A bear (with razor-sharp teeth) in a forest.'; + const tokens = tokenize(prompt); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe(prompt); + }); + + it('should handle colored pencil medium example', () => { + const prompt = 'colored pencil \\(medium\\)'; + const tokens = tokenize(prompt); + const ast = parseTokens(tokens); + const result = serialize(ast); + expect(result).toBe(prompt); + }); + + it('should distinguish between escaped and unescaped in same prompt', () => { + const prompt = 'portrait \\(realistic\\) (high quality)1.2'; + const tokens = tokenize(prompt); + const ast = parseTokens(tokens); + + // Should have escaped parens as nodes and a group with attention + expect(ast).toEqual([ + { type: 'word', text: 'portrait', range: { start: 0, end: 8 }, attention: undefined }, + { type: 'whitespace', value: ' ', range: { start: 8, end: 9 } }, + { type: 'escaped_paren', value: '(', range: { start: 9, end: 11 } }, + { type: 'word', text: 'realistic', range: { start: 11, end: 20 }, attention: undefined }, + { type: 'escaped_paren', value: ')', range: { start: 20, end: 22 } }, + { type: 'whitespace', value: ' ', range: { start: 22, end: 23 } }, + { + type: 'group', + attention: 1.2, + range: { start: 23, end: 40 }, + children: [ + { type: 'word', text: 'high', range: { start: 24, end: 28 }, attention: undefined }, + { type: 'whitespace', value: ' ', range: { start: 28, end: 29 } }, + { type: 'word', text: 'quality', range: { start: 29, end: 36 }, attention: undefined }, + ], + }, + ]); + + const result = serialize(ast); + expect(result).toBe(prompt); + }); + }); +}); diff --git a/invokeai/frontend/web/src/common/util/promptAST.ts b/invokeai/frontend/web/src/common/util/promptAST.ts new file mode 100644 index 00000000000..0a1af621224 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/promptAST.ts @@ -0,0 +1,906 @@ +/** + * Expected as either '+', '-', '++', '--', etc. or a numeric string like '1.2', '0.8', etc. + */ +export type Attention = string | number; + +type Token = + | { type: 'word'; value: string; start: number; end: number } + | { type: 'whitespace'; value: string; start: number; end: number } + | { type: 'punct'; value: string; start: number; end: number } + | { type: 'lparen'; start: number; end: number } + | { type: 'rparen'; start: number; end: number } + | { type: 'weight'; value: Attention; start: number; end: number } + | { type: 'lembed'; start: number; end: number } + | { type: 'rembed'; start: number; end: number } + | { type: 'escaped_paren'; value: '(' | ')'; start: number; end: number }; + +/** + * A single argument in a prompt function like .and(), .or(), or .blend(). + * Contains the parsed AST nodes of the argument content and metadata about quoting/range. + */ +export type PromptFunctionArg = { + nodes: ASTNode[]; + quote: string; + /** Range of the content between the quotes (exclusive of quotes themselves) in original prompt coordinates. */ + contentRange: { start: number; end: number }; + /** Raw separator whitespace after the comma before this arg (args[1+] only). */ + separator?: string; +}; + +export type ASTNode = + | { type: 'word'; text: string; attention?: Attention; range: { start: number; end: number }; isSelection?: boolean } + | { + type: 'group'; + children: ASTNode[]; + attention?: Attention; + range: { start: number; end: number }; + isSelection?: boolean; + } + | { type: 'embedding'; value: string; range: { start: number; end: number }; isSelection?: boolean } + | { type: 'whitespace'; value: string; range: { start: number; end: number }; isSelection?: boolean } + | { type: 'punct'; value: string; range: { start: number; end: number }; isSelection?: boolean } + | { type: 'escaped_paren'; value: '(' | ')'; range: { start: number; end: number }; isSelection?: boolean } + | { + type: 'prompt_function'; + name: string; + promptArgs: PromptFunctionArg[]; + functionParams: string; + range: { start: number; end: number }; + isSelection?: boolean; + }; + +const WEIGHT_PATTERN = /^[+-]?(\d+(\.\d+)?|[+-]+)/; +const WHITESPACE_PATTERN = /^\s+/; +const WORD_CHAR_PATTERN = /[a-zA-Z0-9_]/; +// prettier-ignore +const PUNCTUATION_PATTERN = /^[.,/!?;:'"""''\u2018\u2019\u201c\u201d`~@#$%^&*=_|]/; + +/** All characters that can serve as an opening quote in a prompt function argument. */ +const OPEN_QUOTE_CHARS = new Set(["'", '"', '\u2018', '\u201c']); + +/** Map from opening curly quote to the matching closing curly quote. Straight quotes match themselves. */ +const CLOSE_QUOTE_MAP: Record = { + "'": "'", + '"': '"', + '\u2018': '\u2019', // ' → ' + '\u201c': '\u201d', // " → " +}; + +// #region Token Helpers + +/** Get the string value of a token, if it has one. */ +function tokenValue(t: Token | undefined): string | undefined { + if (!t) { + return undefined; + } + if ('value' in t) { + return String(t.value); + } + return undefined; +} + +/** Check if a token is a punct token with a specific value. */ +function isPunctValue(t: Token | undefined, value: string): boolean { + return t?.type === 'punct' && tokenValue(t) === value; +} + +// #region Tokenizer + +/** + * Convert a prompt string into a token stream. + * @param prompt string + * @returns Token[] + */ +export function tokenize(prompt: string): Token[] { + if (!prompt) { + return []; + } + + const len = prompt.length; + const tokens: Token[] = []; + let i = 0; + + while (i < len) { + const char = prompt[i]; + if (!char) { + break; + } + + const result = + tokenizeWhitespace(char, i) || + tokenizeEscapedParen(prompt, i) || + tokenizeLeftParen(char, i) || + tokenizeRightParen(prompt, i) || + tokenizeEmbedding(char, i) || + tokenizeWord(prompt, i) || + tokenizePunctuation(char, i) || + tokenizeFallback(char, i); + + if (result) { + if (result.token) { + tokens.push(result.token); + } + if (result.extraToken) { + tokens.push(result.extraToken); + } + i = result.nextIndex; + } else { + i++; + } + } + + return tokens; +} + +type TokenizeResult = { + token?: Token; + extraToken?: Token; + nextIndex: number; +} | null; + +function tokenizeWhitespace(char: string, i: number): TokenizeResult { + if (WHITESPACE_PATTERN.test(char)) { + return { + token: { type: 'whitespace', value: char, start: i, end: i + 1 }, + nextIndex: i + 1, + }; + } + return null; +} + +function tokenizeEscapedParen(prompt: string, i: number): TokenizeResult { + const char = prompt[i]; + if (char === '\\' && i + 1 < prompt.length) { + const nextChar = prompt[i + 1]; + if (nextChar === '(' || nextChar === ')') { + return { + token: { type: 'escaped_paren', value: nextChar, start: i, end: i + 2 }, + nextIndex: i + 2, + }; + } + } + return null; +} + +function tokenizeLeftParen(char: string, i: number): TokenizeResult { + if (char === '(') { + return { + token: { type: 'lparen', start: i, end: i + 1 }, + nextIndex: i + 1, + }; + } + return null; +} + +function tokenizeRightParen(prompt: string, i: number): TokenizeResult { + const char = prompt[i]; + if (char === ')') { + // Look ahead for weight like ')1.1' or ')-0.9' or ')+' or ')-' + const weightMatch = prompt.slice(i + 1).match(WEIGHT_PATTERN); + if (weightMatch && weightMatch[0]) { + let weight: Attention = weightMatch[0]; + if (!isNaN(Number(weight))) { + weight = Number(weight); + } + const weightEnd = i + 1 + weightMatch[0].length; + return { + token: { type: 'rparen', start: i, end: i + 1 }, + extraToken: { type: 'weight', value: weight, start: i + 1, end: weightEnd }, + nextIndex: weightEnd, + }; + } + return { + token: { type: 'rparen', start: i, end: i + 1 }, + nextIndex: i + 1, + }; + } + return null; +} + +function tokenizePunctuation(char: string, i: number): TokenizeResult { + if (PUNCTUATION_PATTERN.test(char)) { + return { + token: { type: 'punct', value: char, start: i, end: i + 1 }, + nextIndex: i + 1, + }; + } + return null; +} + +function tokenizeWord(prompt: string, i: number): TokenizeResult { + const char = prompt[i]; + if (!char) { + return null; + } + + if (WORD_CHAR_PATTERN.test(char)) { + let j = i; + while (j < prompt.length && WORD_CHAR_PATTERN.test(prompt[j]!)) { + j++; + } + const word = prompt.slice(i, j); + + // Check for weight immediately after word (e.g., "Lorem+", "consectetur-") + const weightMatch = prompt.slice(j).match(WEIGHT_PATTERN); + if (weightMatch && weightMatch[0]) { + const weightEnd = j + weightMatch[0].length; + return { + token: { type: 'word', value: word, start: i, end: j }, + extraToken: { type: 'weight', value: weightMatch[0], start: j, end: weightEnd }, + nextIndex: weightEnd, + }; + } + + return { + token: { type: 'word', value: word, start: i, end: j }, + nextIndex: j, + }; + } + return null; +} + +function tokenizeEmbedding(char: string, i: number): TokenizeResult { + if (char === '<') { + return { + token: { type: 'lembed', start: i, end: i + 1 }, + nextIndex: i + 1, + }; + } + if (char === '>') { + return { + token: { type: 'rembed', start: i, end: i + 1 }, + nextIndex: i + 1, + }; + } + return null; +} + +/** + * Fallback tokenizer for characters not matched by any other tokenizer. + * Emits them as word tokens so they are preserved in the AST rather than silently dropped. + * This handles non-Latin Unicode text (CJK, emoji, etc.) and any other unrecognized characters. + */ +function tokenizeFallback(char: string, i: number): TokenizeResult { + return { + token: { type: 'word', value: char, start: i, end: i + 1 }, + nextIndex: i + 1, + }; +} + +// #region Parser + +/** + * Convert tokens into an AST. + * @param tokens Token[] + * @returns ASTNode[] + */ +export function parseTokens(tokens: Token[]): ASTNode[] { + let pos = 0; + + function peek(): Token | undefined { + return tokens[pos]; + } + + function peekAt(offset: number): Token | undefined { + return tokens[pos + offset]; + } + + function consume(): Token | undefined { + return tokens[pos++]; + } + + /** + * Quick lookahead check: does the current lparen (already consumed) start a quoted prompt function? + * A quoted prompt function looks like ('...', '...').method(...) + * We check if the first non-whitespace token after lparen is a quote character. + */ + function isQuotedPromptFunctionAhead(): boolean { + let p = 0; + while (peekAt(p)?.type === 'whitespace') { + p++; + } + const t = peekAt(p); + return t?.type === 'punct' && OPEN_QUOTE_CHARS.has(tokenValue(t)!); + } + + /** + * Lookahead check: does the current lparen (already consumed) start an unquoted prompt function? + * An unquoted prompt function looks like (arg1, arg2).method(...) where args are not quoted. + * We scan forward looking for a comma at the same nesting depth, then rparen followed by .word( + */ + function isUnquotedPromptFunctionAhead(): boolean { + let p = 0; + let depth = 0; + let hasComma = false; + + // Scan forward through tokens to find the matching rparen + while (peekAt(p)) { + const t = peekAt(p)!; + + if (t.type === 'lparen') { + depth++; + } else if (t.type === 'rparen') { + if (depth === 0) { + // Found matching rparen — now check for .methodName( pattern + // (possibly with whitespace between ) and .) + if (!hasComma) { + return false; // No comma means it's just a regular group + } + let next = p + 1; + while (peekAt(next)?.type === 'whitespace') { + next++; + } + return ( + isPunctValue(peekAt(next), '.') && peekAt(next + 1)?.type === 'word' && peekAt(next + 2)?.type === 'lparen' + ); + } + depth--; + } else if (isPunctValue(t, ',') && depth === 0) { + hasComma = true; + } + + p++; + } + return false; + } + + /** + * Parse the `.methodName(params)` suffix that follows the closing rparen of a prompt function. + * Assumes whitespace has already been skipped. Returns null and restores pos if the pattern + * doesn't match. + */ + function tryParseMethodTail(savedPos: number): { name: string; functionParams: string; endPos: number } | null { + // Skip whitespace between ) and .methodName (allows newlines) + while (peek()?.type === 'whitespace') { + consume(); + } + + // Expect .methodName(params) + if (!isPunctValue(peek(), '.')) { + pos = savedPos; + return null; + } + consume(); // consume dot + + if (peek()?.type !== 'word') { + pos = savedPos; + return null; + } + const methodName = tokenValue(consume())!; + + // Expect opening paren for method call + if (peek()?.type !== 'lparen') { + pos = savedPos; + return null; + } + consume(); // consume method open paren + + // Collect method params until closing rparen + let functionParams = ''; + while (pos < tokens.length) { + const t = peek()!; + if (t.type === 'rparen') { + break; + } + const tok = consume()!; + const v = tokenValue(tok); + if (v !== undefined) { + functionParams += v; + } + } + + // Expect closing rparen for method call + if (peek()?.type !== 'rparen') { + pos = savedPos; + return null; + } + const methodCloseParen = consume()!; // consume method close paren + + return { name: methodName, functionParams, endPos: methodCloseParen.end }; + } + + /** + * Try to parse a prompt function starting after the opening lparen. + * Returns the PromptFunctionNode if successful, or null if the pattern doesn't match + * (in which case `pos` is restored to `savedPos`). + */ + function tryParsePromptFunction(lparenToken: Token & { type: 'lparen' }, savedPos: number): ASTNode | null { + const args: PromptFunctionArg[] = []; + let openQuoteChar: string | null = null; + let closeQuoteChar: string | null = null; + let pendingSeparator: string | undefined; + + while (pos < tokens.length) { + // Skip whitespace before arg or closing paren + while (peek()?.type === 'whitespace') { + consume(); + } + + // Check for rparen (end of prompt function args) + if (peek()?.type === 'rparen') { + break; + } + + // Expect comma separator between args + if (args.length > 0) { + if (isPunctValue(peek(), ',')) { + consume(); + let sep = ''; + while (peek()?.type === 'whitespace') { + const sepToken = consume()!; + const sepValue = tokenValue(sepToken); + if (sepValue !== undefined) { + sep += sepValue; + } + } + pendingSeparator = sep; + } else { + pos = savedPos; + return null; + } + } + + // Expect opening quote + const openQuoteTok = peek(); + if (!openQuoteTok || openQuoteTok.type !== 'punct') { + pos = savedPos; + return null; + } + const thisOpenQuote = tokenValue(openQuoteTok)!; + if (!OPEN_QUOTE_CHARS.has(thisOpenQuote)) { + pos = savedPos; + return null; + } + + const thisCloseQuote = CLOSE_QUOTE_MAP[thisOpenQuote]!; + if (openQuoteChar === null) { + openQuoteChar = thisOpenQuote; + closeQuoteChar = thisCloseQuote; + } else if (thisOpenQuote !== openQuoteChar) { + // Mismatched quote style between args + pos = savedPos; + return null; + } + + consume(); // consume opening quote + const contentStart = openQuoteTok.end; + + // Collect tokens until closing quote + const argTokens: Token[] = []; + let contentEnd = contentStart; + while (pos < tokens.length) { + const t = peek(); + if (isPunctValue(t, closeQuoteChar!)) { + contentEnd = t!.start; + break; + } + const consumed = consume()!; + argTokens.push(consumed); + contentEnd = consumed.end; + } + + // Expect closing quote + if (!isPunctValue(peek(), closeQuoteChar!)) { + pos = savedPos; + return null; + } + consume(); // consume closing quote + + // Parse sub-tokens as AST + const argNodes = parseTokens(argTokens); + + args.push({ + nodes: argNodes, + quote: openQuoteChar, + contentRange: { start: contentStart, end: contentEnd }, + separator: pendingSeparator, + }); + pendingSeparator = undefined; + } + + if (args.length === 0) { + pos = savedPos; + return null; + } + + // Expect rparen + if (peek()?.type !== 'rparen') { + pos = savedPos; + return null; + } + consume(); // consume rparen + + // Parse .methodName(params) suffix + const methodTail = tryParseMethodTail(savedPos); + if (!methodTail) { + return null; // pos already restored by tryParseMethodTail + } + + return { + type: 'prompt_function', + name: methodTail.name, + promptArgs: args, + functionParams: methodTail.functionParams, + range: { start: lparenToken.start, end: methodTail.endPos }, + }; + } + + /** + * Try to parse an unquoted prompt function starting after the opening lparen. + * Unquoted prompt functions look like (arg1 words, arg2 words).method(params) + * where arguments are separated by commas without quotes. + * Returns the PromptFunctionNode if successful, or null if the pattern doesn't match + * (in which case `pos` is restored to `savedPos`). + */ + function tryParseUnquotedPromptFunction(lparenToken: Token & { type: 'lparen' }, savedPos: number): ASTNode | null { + const args: PromptFunctionArg[] = []; + let pendingSeparator: string | undefined; + + while (pos < tokens.length) { + // Check for rparen (end of prompt function args) + if (peek()?.type === 'rparen') { + break; + } + + // Expect comma separator between args (consume the comma) + if (args.length > 0) { + if (isPunctValue(peek(), ',')) { + consume(); // consume comma + let sep = ''; + while (peek()?.type === 'whitespace') { + const sepToken = consume()!; + const sepValue = tokenValue(sepToken); + if (sepValue !== undefined) { + sep += sepValue; + } + } + pendingSeparator = sep; + } else { + pos = savedPos; + return null; + } + } + + // Collect tokens until comma or rparen (at nesting depth 0) + const argTokens: Token[] = []; + let contentStart: number | null = null; + let contentEnd: number | null = null; + let depth = 0; + + while (pos < tokens.length) { + const t = peek()!; + + if (t.type === 'lparen') { + depth++; + } else if (t.type === 'rparen') { + if (depth === 0) { + break; // End of all args + } + depth--; + } else if (isPunctValue(t, ',') && depth === 0) { + break; // End of this arg + } + + if (contentStart === null) { + contentStart = t.start; + } + const consumed = consume()!; + argTokens.push(consumed); + contentEnd = consumed.end; + } + + if (argTokens.length === 0) { + pos = savedPos; + return null; + } + + // Trim leading/trailing whitespace tokens from the arg content + let firstNonWs = 0; + while (firstNonWs < argTokens.length && argTokens[firstNonWs]!.type === 'whitespace') { + firstNonWs++; + } + let lastNonWs = argTokens.length - 1; + while (lastNonWs >= 0 && argTokens[lastNonWs]!.type === 'whitespace') { + lastNonWs--; + } + + const trimmedArgTokens = argTokens.slice(firstNonWs, lastNonWs + 1); + const trimmedStart = trimmedArgTokens.length > 0 ? trimmedArgTokens[0]!.start : contentStart!; + const trimmedEnd = trimmedArgTokens.length > 0 ? trimmedArgTokens[trimmedArgTokens.length - 1]!.end : contentEnd!; + + // Parse sub-tokens as AST + const argNodes = parseTokens(trimmedArgTokens); + + args.push({ + nodes: argNodes, + quote: '', // Unquoted + contentRange: { start: trimmedStart, end: trimmedEnd }, + separator: pendingSeparator, + }); + pendingSeparator = undefined; + } + + if (args.length < 2) { + // An unquoted prompt function must have at least 2 args (otherwise it's a regular group) + pos = savedPos; + return null; + } + + // Expect rparen + if (peek()?.type !== 'rparen') { + pos = savedPos; + return null; + } + consume(); // consume rparen + + // Parse .methodName(params) suffix + const methodTail = tryParseMethodTail(savedPos); + if (!methodTail) { + return null; // pos already restored by tryParseMethodTail + } + + return { + type: 'prompt_function', + name: methodTail.name, + promptArgs: args, + functionParams: methodTail.functionParams, + range: { start: lparenToken.start, end: methodTail.endPos }, + }; + } + + function parseGroup(): ASTNode[] { + const nodes: ASTNode[] = []; + + while (pos < tokens.length) { + const token = peek(); + if (!token || token.type === 'rparen') { + break; + } + + switch (token.type) { + case 'whitespace': { + const wsToken = consume() as Token & { type: 'whitespace' }; + nodes.push({ type: 'whitespace', value: wsToken.value, range: { start: wsToken.start, end: wsToken.end } }); + break; + } + case 'lparen': { + const lparen = consume() as Token & { type: 'lparen' }; + + // Try to parse as a quoted prompt function first + if (isQuotedPromptFunctionAhead()) { + const savedPos = pos; + const pfResult = tryParsePromptFunction(lparen, savedPos); + if (pfResult) { + nodes.push(pfResult); + break; + } + // pos was restored by tryParsePromptFunction on failure + } + + // Try to parse as an unquoted prompt function + if (isUnquotedPromptFunctionAhead()) { + const savedPos = pos; + const pfResult = tryParseUnquotedPromptFunction(lparen, savedPos); + if (pfResult) { + nodes.push(pfResult); + break; + } + // pos was restored by tryParseUnquotedPromptFunction on failure + } + + // Regular group parsing + const groupChildren = parseGroup(); + + let attention: Attention | undefined; + let end = lparen.end; // Default end if no rparen + + if (peek()?.type === 'rparen') { + const rparen = consume() as Token & { type: 'rparen' }; + end = rparen.end; + if (peek()?.type === 'weight') { + const weightToken = consume() as Token & { type: 'weight' }; + attention = weightToken.value; + end = weightToken.end; + } + } + + // If we hit EOF without rparen, the group extends to the end of the last child + if (end === lparen.end && groupChildren.length > 0) { + end = groupChildren[groupChildren.length - 1]!.range.end; + } + + nodes.push({ type: 'group', children: groupChildren, attention, range: { start: lparen.start, end } }); + break; + } + case 'lembed': { + const lembed = consume() as Token & { type: 'lembed' }; + let embedValue = ''; + let end = lembed.end; + while (peek() && peek()!.type !== 'rembed') { + const embedToken = consume()!; + const v = tokenValue(embedToken); + if (v !== undefined) { + embedValue += v; + } + end = embedToken.end; + } + if (peek()?.type === 'rembed') { + const rembed = consume() as Token & { type: 'rembed' }; + end = rembed.end; + } + nodes.push({ type: 'embedding', value: embedValue.trim(), range: { start: lembed.start, end } }); + break; + } + case 'word': { + const wordToken = consume() as Token & { type: 'word' }; + let attention: Attention | undefined; + let end = wordToken.end; + + // Check for immediate weight after word + if (peek()?.type === 'weight') { + const weightToken = consume() as Token & { type: 'weight' }; + attention = weightToken.value; + end = weightToken.end; + } + + nodes.push({ type: 'word', text: wordToken.value, attention, range: { start: wordToken.start, end } }); + break; + } + case 'punct': { + const punctToken = consume() as Token & { type: 'punct' }; + nodes.push({ + type: 'punct', + value: punctToken.value, + range: { start: punctToken.start, end: punctToken.end }, + }); + break; + } + case 'escaped_paren': { + const escapedToken = consume() as Token & { type: 'escaped_paren' }; + nodes.push({ + type: 'escaped_paren', + value: escapedToken.value, + range: { start: escapedToken.start, end: escapedToken.end }, + }); + break; + } + default: { + consume(); + } + } + } + + return nodes; + } + + return parseGroup(); +} + +// #region Serialization + +/** + * Visitor callbacks for AST serialization. All callbacks are optional. + * Called during traversal to allow tracking node positions in the output string. + */ +type SerializeVisitor = { + /** Called after a node has been fully serialized, with its start and end positions in the output. */ + onNode?: (node: ASTNode, start: number, end: number) => void; +}; + +/** Mutable buffer used by serializeCore so all recursive calls share the same position tracking. */ +type SerializeBuffer = { prompt: string }; + +/** + * Shared serialization core. Converts an AST back into a prompt string, + * optionally calling visitor hooks for position tracking. + * + * Uses a shared mutable buffer so that node positions reported via + * `visitor.onNode` are always absolute offsets in the final output string, + * even for nodes nested inside groups or prompt function args. + */ +function serializeCore(ast: ASTNode[], visitor: SerializeVisitor | undefined, buf: SerializeBuffer): void { + for (const node of ast) { + const nodeStart = buf.prompt.length; + + switch (node.type) { + case 'punct': + case 'whitespace': { + buf.prompt += node.value; + break; + } + case 'escaped_paren': { + buf.prompt += `\\${node.value}`; + break; + } + case 'word': { + buf.prompt += node.text; + if (node.attention) { + buf.prompt += String(node.attention); + } + break; + } + case 'group': { + buf.prompt += '('; + serializeCore(node.children, visitor, buf); + buf.prompt += ')'; + if (node.attention) { + buf.prompt += String(node.attention); + } + break; + } + case 'embedding': { + buf.prompt += `<${node.value}>`; + break; + } + case 'prompt_function': { + buf.prompt += '('; + for (let i = 0; i < node.promptArgs.length; i++) { + if (i > 0) { + const sep = node.promptArgs[i]!.separator ?? ' '; + buf.prompt += `,${sep}`; + } + const arg = node.promptArgs[i]!; + buf.prompt += arg.quote; + serializeCore(arg.nodes, visitor, buf); + buf.prompt += CLOSE_QUOTE_MAP[arg.quote] ?? arg.quote; + } + buf.prompt += ').'; + buf.prompt += node.name; + buf.prompt += '('; + buf.prompt += node.functionParams; + buf.prompt += ')'; + break; + } + } + + visitor?.onNode?.(node, nodeStart, buf.prompt.length); + } +} + +/** + * Convert an AST back into a prompt string. + * @param ast ASTNode[] + * @returns string + */ +export function serialize(ast: ASTNode[]): string { + const buf: SerializeBuffer = { prompt: '' }; + serializeCore(ast, undefined, buf); + return buf.prompt; +} + +/** + * Serialize an AST to a prompt string while simultaneously computing the + * selection range from `isSelection` flags on nodes. + * + * This is more reliable than separate serialize + selection computation because + * the position tracking is guaranteed to match the serialized output. + */ +export function serializeWithSelection(ast: ASTNode[]): { + prompt: string; + selectionStart: number; + selectionEnd: number; +} { + let selStart = Infinity; + let selEnd = -1; + + const buf: SerializeBuffer = { prompt: '' }; + serializeCore( + ast, + { + onNode(node, start, end) { + if (node.isSelection) { + selStart = Math.min(selStart, start); + selEnd = Math.max(selEnd, end); + } + }, + }, + buf + ); + + if (selStart === Infinity) { + selStart = 0; + selEnd = buf.prompt.length; + } + + return { prompt: buf.prompt, selectionStart: selStart, selectionEnd: selEnd }; +} diff --git a/invokeai/frontend/web/src/common/util/promptAttention.test.ts b/invokeai/frontend/web/src/common/util/promptAttention.test.ts new file mode 100644 index 00000000000..6e165872ec7 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/promptAttention.test.ts @@ -0,0 +1,707 @@ +import { describe, expect, it } from 'vitest'; + +import { adjustPromptAttention } from './promptAttention'; + +/** + * Helper: select by substring match within the prompt. + * If `selected` is a string, finds it in the prompt and uses its position. + * If `selected` is a [start, end] tuple, uses those positions directly. + */ +function adj( + prompt: string, + selected: string | [number, number], + direction: 'increment' | 'decrement', + prefersNumericWeights = false +) { + const [start, end] = + typeof selected === 'string' ? [prompt.indexOf(selected), prompt.indexOf(selected) + selected.length] : selected; + return adjustPromptAttention(prompt, start, end, direction, prefersNumericWeights); +} + +/** Helper that calls adj with prefersNumericWeights=true */ +function adjNumeric(prompt: string, selected: string | [number, number], direction: 'increment' | 'decrement') { + return adj(prompt, selected, direction, true); +} + +describe('adjustPromptAttention', () => { + // Basic Attention + + describe('single word', () => { + it.each([ + ['hello world', 'hello', 'increment', 'hello+ world'], + ['hello world', 'hello', 'decrement', 'hello- world'], + ['hello+ world', 'hello+', 'increment', 'hello++ world'], + ['hello+ world', 'hello+', 'decrement', 'hello world'], + ['hello- world', 'hello-', 'decrement', 'hello-- world'], + ['hello- world', 'hello-', 'increment', 'hello world'], + ] as const)('%s [%s] %s → %s', (prompt, selected, direction, expected) => { + expect(adj(prompt, selected, direction).prompt).toBe(expected); + }); + }); + + describe('multiple words', () => { + it.each([ + ['hello world', [0, 11] as [number, number], 'increment', '(hello world)+'], + ['hello world', [0, 11] as [number, number], 'decrement', '(hello world)-'], + ] as const)('%s [%s] %s → %s', (prompt, selected, direction, expected) => { + expect(adj(prompt, selected, direction).prompt).toBe(expected); + }); + }); + + describe('cursor at word-punctuation boundary', () => { + it('should select word, not punctuation, when cursor is between word and comma', () => { + // "one|, two" — cursor at position 3, between "one" (0-3) and "," (3-4) + expect(adj('one, two', [3, 3], 'increment').prompt).toBe('one+, two'); + }); + + it('should select word, not punctuation, when cursor is between word and period', () => { + expect(adj('one. two', [3, 3], 'increment').prompt).toBe('one+. two'); + }); + + it('should select word when cursor is at start of word after punctuation', () => { + // "one, |two" — cursor at position 5, between " " (4-5) and "two" (5-8) + expect(adj('one, two', [5, 5], 'increment').prompt).toBe('one, two+'); + }); + + it('should still select punctuation when cursor is only touching punctuation', () => { + // Cursor in the middle of a run of punctuation with no adjacent word + // e.g. "one ,, two" cursor at position 5 — between "," (4-5) and "," (5-6) + // Both neighbors are punct, so no word to prefer — should still work + const result = adj('one ,, two', [5, 5], 'increment'); + expect(result).toBeDefined(); + }); + }); + + // Existing Groups + + describe('existing groups', () => { + it('should increment group when cursor is at group boundary', () => { + expect(adj('(hello world)+', [13, 14], 'increment').prompt).toBe('(hello world)++'); + }); + + it('should remove group when attention becomes neutral', () => { + expect(adj('(hello world)+', [0, 14], 'decrement').prompt).toBe('hello world'); + }); + + it('should increment inner word within group', () => { + const result = adj('(a b)+', [1, 2], 'increment'); + expect(result.prompt).toBe('(a+ b)+'); + }); + }); + + // Cross-Boundary Selection + + describe('cross-boundary selection', () => { + it.each([ + // Selection from inside group to outside + ['(a b)+ c', [3, 8], 'increment', '(a b+ c)+'], + ['(a b)+ c', [3, 8], 'decrement', 'a+ b c-'], + // Selection from outside to inside group + ['a (b c)+', [0, 4], 'increment', '(a b+ c)+'], + ['a (b c)+', [0, 4], 'decrement', 'a- b c+'], + // Nested groups + ['((a b)+)+ c', [2, 11], 'increment', '((a b)++ c)+'], + ['((a b)+)+ c', [2, 11], 'decrement', '(a b)+ c-'], + // Spanning multiple groups + ['(a)+ (b)+', [0, 9], 'increment', '(a b)++'], + ['(a)+ (b)+', [0, 9], 'decrement', 'a b'], + // Negative groups + ['(a b)- c', [3, 8], 'decrement', '(a b- c)-'], + ['(a b)- c', [3, 8], 'increment', 'a- b c+'], + // Multiple non-selected items in group + ['(a b c)+ d', [5, 10], 'decrement', '(a b)+ c d-'], + // Word with existing attention crossing boundary + ['c (d- e)+', [0, 5], 'increment', 'c+ d e+'], + // Complex multi-group + ['(a+ b)+ c (d- e)+', [8, 14], 'increment', '(a+ b c)+ d e+'], + ] as const)('%s [%s] %s → %s', (prompt, selected, direction, expected) => { + expect(adj(prompt, selected as string | [number, number], direction).prompt).toBe(expected); + }); + }); + + // Selection Preservation + + describe('selection preservation', () => { + it('should track selection when incrementing single word', () => { + const result = adj('hello world', 'hello', 'increment'); + expect(result.prompt).toBe('hello+ world'); + expect(result.prompt.slice(result.selectionStart, result.selectionEnd)).toBe('hello+'); + }); + + it('should track selection when incrementing full group', () => { + const result = adj('(hello world)+', [0, 14], 'increment'); + expect(result.prompt).toBe('(hello world)++'); + expect(result.prompt.slice(result.selectionStart, result.selectionEnd)).toBe('(hello world)++'); + }); + + it('should track selection when splitting group', () => { + const result = adj('(a b)+', [1, 2], 'increment'); + expect(result.prompt).toBe('(a+ b)+'); + expect(result.prompt.slice(result.selectionStart, result.selectionEnd)).toBe('a+'); + }); + }); + + // Numeric Attention Weights + + describe('numeric attention weights', () => { + it.each([ + // Increment / decrement numeric weights with additive step + ['(masterpiece)1.3', [0, 16], 'increment', '(masterpiece)1.4'], + ['(masterpiece)1.3', [0, 16], 'decrement', '(masterpiece)1.2'], + ['(high detail)1.2', [0, 16], 'increment', '(high detail)1.3'], + ['(sunny midday light)1.15', [0, 24], 'increment', '(sunny midday light)1.25'], + ['(sunny midday light)1.15', [0, 24], 'decrement', '(sunny midday light)1.05'], + ] as const)('%s [%s] %s → %s', (prompt, selected, direction, expected) => { + expect(adj(prompt, selected as [number, number], direction).prompt).toBe(expected); + }); + + it('should preserve non-selected numeric weights when adjusting elsewhere', () => { + const prompt = '(masterpiece)1.3, best quality'; + const result = adj(prompt, 'best quality', 'increment'); + expect(result.prompt).toContain('(masterpiece)1.3'); + expect(result.prompt).not.toContain('masterpiece1.3'); + }); + + it('should not produce floating point garbage', () => { + const prompt = '(high detail)1.2, oil painting'; + const result = adj(prompt, 'oil painting', 'increment'); + expect(result.prompt).toContain('(high detail)1.2'); + expect(result.prompt).not.toMatch(/1\.19999/); + expect(result.prompt).not.toMatch(/1\.20000/); + }); + + it('should preserve numeric weight 1.15 without corruption', () => { + const prompt = '(sunny midday light)1.15, landscape'; + const result = adj(prompt, 'landscape', 'increment'); + expect(result.prompt).toContain('(sunny midday light)1.15'); + expect(result.prompt).not.toMatch(/1\.15005/); + }); + + it('should normalize numeric 1.1 weight to + syntax', () => { + const prompt = '(lush rolling hills)1.1, landscape'; + const result = adj(prompt, 'landscape', 'increment'); + expect(result.prompt).toMatch(/\(lush rolling hills\)(\+|1\.1)/); + }); + + it('should handle the full complex prompt without corrupting non-selected weights', () => { + const prompt = + '(masterpiece)1.3, best quality, (high detail)1.2, oil painting, (sunny midday light)1.15, an old stone castle standing on a hill, medieval architecture, weathered stone walls, (lush rolling hills)1.1, expansive landscape, clear blue sky'; + const result = adj(prompt, 'clear blue sky', 'increment'); + + expect(result.prompt).toContain('(masterpiece)1.3'); + expect(result.prompt).toContain('(high detail)1.2'); + expect(result.prompt).toContain('(sunny midday light)1.15'); + expect(result.prompt).toContain('(clear blue sky)+'); + expect(result.prompt).not.toMatch(/\d\.\d{5,}/); + }); + }); + + // Prompt Functions + + describe('prompt functions', () => { + describe('within a single argument', () => { + it.each([ + // Single word inside an arg + ["('hello world', 'other').and()", 'hello', 'increment', "('hello+ world', 'other').and()"], + ["('hello world', 'other').and()", 'hello', 'decrement', "('hello- world', 'other').and()"], + // Multiple words in second arg + ["('a', 'hello world').or()", 'hello world', 'increment', "('a', '(hello world)+').or()"], + ["('a', 'hello world').or()", 'hello world', 'decrement', "('a', '(hello world)-').or()"], + // Single word in .blend() + ["('one two', 'three four').blend(0.7, 0.3)", 'two', 'increment', "('one two+', 'three four').blend(0.7, 0.3)"], + ] as const)('%s [%s] %s → %s', (prompt, selected, direction, expected) => { + expect(adj(prompt, selected, direction).prompt).toBe(expected); + }); + }); + + describe('across argument separator', () => { + it('should adjust both args simultaneously when selection spans separator (increment)', () => { + const prompt = "('one two', 'three four').and()"; + // Select across the separator: "two', 'three" + const start = prompt.indexOf('two'); + const end = prompt.indexOf('three') + 'three'.length; + const result = adjustPromptAttention(prompt, start, end, 'increment'); + expect(result.prompt).toBe("('one two+', 'three+ four').and()"); + }); + + it('should adjust both args simultaneously when selection spans separator (decrement)', () => { + const prompt = "('one two', 'three four').and()"; + const start = prompt.indexOf('two'); + const end = prompt.indexOf('three') + 'three'.length; + const result = adjustPromptAttention(prompt, start, end, 'decrement'); + expect(result.prompt).toBe("('one two-', 'three- four').and()"); + }); + + it('should adjust across separator for .or()', () => { + const prompt = "('alpha beta', 'gamma delta').or()"; + const start = prompt.indexOf('beta'); + const end = prompt.indexOf('gamma') + 'gamma'.length; + const result = adjustPromptAttention(prompt, start, end, 'increment'); + expect(result.prompt).toBe("('alpha beta+', 'gamma+ delta').or()"); + }); + + it('should adjust across separator for .blend() preserving params', () => { + const prompt = "('one two', 'three four').blend(0.7, 0.3)"; + const start = prompt.indexOf('two'); + const end = prompt.indexOf('three') + 'three'.length; + const result = adjustPromptAttention(prompt, start, end, 'increment'); + expect(result.prompt).toBe("('one two+', 'three+ four').blend(0.7, 0.3)"); + }); + + it('should handle repeated increment across separator', () => { + const prompt = "('one two+', 'three+ four').and()"; + const start = prompt.indexOf('two'); + const end = prompt.indexOf('three') + 'three'.length; + // "two+" is at the boundary, "three+" is at the boundary + const result = adjustPromptAttention(prompt, start, end, 'increment'); + expect(result.prompt).toBe("('one two++', 'three++ four').and()"); + }); + }); + + describe('whole function selected', () => { + it('should increment all content in all args when whole function is selected', () => { + const prompt = "('one', 'two').and()"; + const result = adjustPromptAttention(prompt, 0, prompt.length, 'increment'); + expect(result.prompt).toBe("('one+', 'two+').and()"); + }); + + it('should decrement all content in all args', () => { + const prompt = "('one', 'two').and()"; + const result = adjustPromptAttention(prompt, 0, prompt.length, 'decrement'); + expect(result.prompt).toBe("('one-', 'two-').and()"); + }); + + it('should increment all args of .blend() preserving params', () => { + const prompt = "('one', 'two').blend(0.7, 0.3)"; + const result = adjustPromptAttention(prompt, 0, prompt.length, 'increment'); + expect(result.prompt).toBe("('one+', 'two+').blend(0.7, 0.3)"); + }); + }); + + describe('prompt function embedded in larger prompt', () => { + it('should adjust only the targeted region outside the function', () => { + const prompt = "some text, ('a', 'b').and(), more text"; + const result = adj(prompt, 'some', 'increment'); + expect(result.prompt).toContain('some+'); + expect(result.prompt).toContain("('a', 'b').and()"); + }); + + it('should adjust only the targeted region inside the function', () => { + const prompt = "prefix ('alpha beta', 'gamma').and() suffix"; + const result = adj(prompt, 'alpha', 'increment'); + expect(result.prompt).toContain("'alpha+ beta'"); + expect(result.prompt).toContain('prefix'); + expect(result.prompt).toContain('suffix'); + }); + + it('should adjust text outside and inside function when selection spans boundary', () => { + const prompt = "text ('one two', 'three').and()"; + // Select from 'text' through 'one' + const start = prompt.indexOf('text'); + const end = prompt.indexOf('one') + 'one'.length; + const result = adjustPromptAttention(prompt, start, end, 'increment'); + expect(result.prompt).toContain('text+'); + expect(result.prompt).toContain("'one+ two'"); + }); + }); + + describe('prompt function with existing attention inside args', () => { + it('should further increment already-weighted word inside arg', () => { + const prompt = "('hello+', 'world').and()"; + // Select hello+ (the word with its weight marker) + const result = adj(prompt, 'hello+', 'increment'); + expect(result.prompt).toBe("('hello++', 'world').and()"); + }); + + it('should cancel attention to neutral inside arg', () => { + const prompt = "('hello+', 'world').and()"; + const result = adj(prompt, 'hello+', 'decrement'); + expect(result.prompt).toBe("('hello', 'world').and()"); + }); + + it('should handle group attention inside arg', () => { + const prompt = "('(a b)+', 'c').and()"; + // Select everything in first arg + const start = prompt.indexOf('(a b)+'); + const end = start + '(a b)+'.length; + const result = adjustPromptAttention(prompt, start, end, 'increment'); + expect(result.prompt).toBe("('(a b)++', 'c').and()"); + }); + }); + + describe('three-arg prompt functions', () => { + it('should adjust a word in one arg of a three-arg blend', () => { + const prompt = "('a', 'b', 'c').blend(0.5, 0.3, 0.2)"; + const result = adj(prompt, 'b', 'increment'); + expect(result.prompt).toBe("('a', 'b+', 'c').blend(0.5, 0.3, 0.2)"); + }); + + it('should adjust across two separators in a three-arg blend', () => { + const prompt = "('aa bb', 'cc dd', 'ee ff').blend(0.5, 0.3, 0.2)"; + // Select from bb through ee + const start = prompt.indexOf('bb'); + const end = prompt.indexOf('ee') + 'ee'.length; + const result = adjustPromptAttention(prompt, start, end, 'increment'); + expect(result.prompt).toBe("('aa bb+', '(cc dd)+', 'ee+ ff').blend(0.5, 0.3, 0.2)"); + }); + }); + + describe('unquoted prompt functions', () => { + it('should increment a word in unquoted .and()', () => { + const prompt = '(one, two).and()'; + const result = adj(prompt, 'one', 'increment'); + expect(result.prompt).toBe('(one+, two).and()'); + }); + + it('should decrement a word in unquoted .and()', () => { + const prompt = '(one, two).and()'; + const result = adj(prompt, 'one', 'decrement'); + expect(result.prompt).toBe('(one-, two).and()'); + }); + + it('should increment a word in unquoted multi-word arg', () => { + const prompt = '(hello world, foo bar).and()'; + const result = adj(prompt, 'hello', 'increment'); + expect(result.prompt).toBe('(hello+ world, foo bar).and()'); + }); + + it('should increment all args when whole unquoted function is selected', () => { + const prompt = '(one, two).and()'; + const result = adjustPromptAttention(prompt, 0, prompt.length, 'increment'); + expect(result.prompt).toBe('(one+, two+).and()'); + }); + + it('should preserve unquoted prompt function when adjusting text outside', () => { + const prompt = 'prefix (a, b).and() suffix'; + const result = adj(prompt, 'prefix', 'increment'); + expect(result.prompt).toContain('(a, b).and()'); + expect(result.prompt).toContain('prefix+'); + }); + + it('should handle unquoted .blend() with params', () => { + const prompt = '(one two, three four).blend(0.7, 0.3)'; + const result = adj(prompt, 'one', 'increment'); + expect(result.prompt).toBe('(one+ two, three four).blend(0.7, 0.3)'); + }); + + it('should adjust across separator in unquoted prompt function', () => { + const prompt = '(one two, three four).and()'; + const start = prompt.indexOf('two'); + const end = prompt.indexOf('three') + 'three'.length; + const result = adjustPromptAttention(prompt, start, end, 'increment'); + expect(result.prompt).toBe('(one two+, three+ four).and()'); + }); + }); + + describe('curly-quoted prompt functions', () => { + it('should increment a word inside curly double-quoted arg', () => { + const prompt = '(\u201chello world\u201d, \u201cother\u201d).and()'; + const result = adj(prompt, 'hello', 'increment'); + expect(result.prompt).toBe('(\u201chello+ world\u201d, \u201cother\u201d).and()'); + }); + + it('should decrement a word inside curly double-quoted arg', () => { + const prompt = '(\u201chello world\u201d, \u201cother\u201d).and()'; + const result = adj(prompt, 'hello', 'decrement'); + expect(result.prompt).toBe('(\u201chello- world\u201d, \u201cother\u201d).and()'); + }); + + it('should increment a word inside curly single-quoted arg', () => { + const prompt = '(\u2018hello world\u2019, \u2018other\u2019).and()'; + const result = adj(prompt, 'hello', 'increment'); + expect(result.prompt).toBe('(\u2018hello+ world\u2019, \u2018other\u2019).and()'); + }); + + it('should increment all args when whole curly-quoted function is selected', () => { + const prompt = '(\u201cone\u201d, \u201ctwo\u201d).and()'; + const result = adjustPromptAttention(prompt, 0, prompt.length, 'increment'); + expect(result.prompt).toBe('(\u201cone+\u201d, \u201ctwo+\u201d).and()'); + }); + + it('should adjust across separator in curly double-quoted prompt function', () => { + const prompt = '(\u201cone two\u201d, \u201cthree four\u201d).and()'; + const start = prompt.indexOf('two'); + const end = prompt.indexOf('three') + 'three'.length; + const result = adjustPromptAttention(prompt, start, end, 'increment'); + expect(result.prompt).toBe('(\u201cone two+\u201d, \u201cthree+ four\u201d).and()'); + }); + + it('should preserve curly-quoted function when adjusting text outside', () => { + const prompt = 'prefix (\u201ca\u201d, \u201cb\u201d).and() suffix'; + const result = adj(prompt, 'prefix', 'increment'); + expect(result.prompt).toContain('(\u201ca\u201d, \u201cb\u201d).and()'); + expect(result.prompt).toContain('prefix+'); + }); + + it('should handle curly-quoted .blend() with params', () => { + const prompt = '(\u201cone two\u201d, \u201cthree four\u201d).blend(0.7, 0.3)'; + const result = adj(prompt, 'one', 'increment'); + expect(result.prompt).toBe('(\u201cone+ two\u201d, \u201cthree four\u201d).blend(0.7, 0.3)'); + }); + }); + + describe('newline before .method()', () => { + it('should increment a word in quoted prompt function with newline before .method()', () => { + const prompt = "('hello world', 'other')\n.and()"; + const result = adj(prompt, 'hello', 'increment'); + // Newline is normalized away in output + expect(result.prompt).toBe("('hello+ world', 'other').and()"); + }); + + it('should increment a word in curly-quoted prompt function with newline before .method()', () => { + const prompt = '(\u201chello world\u201d, \u201cother\u201d)\n.and()'; + const result = adj(prompt, 'hello', 'increment'); + expect(result.prompt).toBe('(\u201chello+ world\u201d, \u201cother\u201d).and()'); + }); + + it('should increment a word in unquoted prompt function with newline before .method()', () => { + const prompt = '(hello, other)\n.and()'; + const result = adj(prompt, 'hello', 'increment'); + expect(result.prompt).toBe('(hello+, other).and()'); + }); + }); + + describe('paragraph separators between args', () => { + it('should preserve newlines between quoted args when adjusting', () => { + const prompt = "('chunk 1\n\nline',\n 'chunk 2').and()"; + const result = adj(prompt, 'chunk', 'increment'); + expect(result.prompt).toBe("('chunk+ 1\n\nline',\n 'chunk 2').and()"); + }); + }); + }); + + // Selection Preservation with Prompt Functions + + describe('selection preservation with prompt functions', () => { + it('should track selection for single word inside prompt function arg', () => { + const prompt = "('hello world', 'other').and()"; + const result = adj(prompt, 'hello', 'increment'); + expect(result.prompt).toBe("('hello+ world', 'other').and()"); + expect(result.prompt.slice(result.selectionStart, result.selectionEnd)).toBe('hello+'); + }); + + it('should track selection spanning across prompt function separator', () => { + const prompt = "('one two', 'three four').and()"; + const start = prompt.indexOf('two'); + const end = prompt.indexOf('three') + 'three'.length; + const result = adjustPromptAttention(prompt, start, end, 'increment'); + expect(result.prompt).toBe("('one two+', 'three+ four').and()"); + // Selection should span from 'two+' through 'three+' (including structural chars between) + const sel = result.prompt.slice(result.selectionStart, result.selectionEnd); + expect(sel).toContain('two+'); + expect(sel).toContain('three+'); + }); + }); + + // Edge Cases + + describe('edge cases', () => { + it('should return prompt unchanged when no selection overlap', () => { + const prompt = 'hello world'; + const result = adjustPromptAttention(prompt, 5, 5, 'increment'); + // Cursor at the boundary between hello and space — should still find a terminal + expect(result.prompt).toBeDefined(); + }); + + it('should handle empty prompt', () => { + const result = adjustPromptAttention('', 0, 0, 'increment'); + expect(result.prompt).toBe(''); + }); + + it('should not modify prompt function structure when cursor is on structural char', () => { + const prompt = "('a', 'b').and()"; + // Cursor on the dot between ) and and + const dotPos = prompt.indexOf('.and'); + const result = adjustPromptAttention(prompt, dotPos, dotPos, 'increment'); + // Should either not change or only affect content, not break the structure + expect(result.prompt).toContain('.and()'); + }); + }); + + // Numeric Weight Preference + + describe('prefersNumericWeights', () => { + describe('single word (no existing attention)', () => { + it.each([ + ['hello world', 'hello', 'increment', '(hello)1.1 world'], + ['hello world', 'hello', 'decrement', '(hello)0.9 world'], + ['hello world', 'world', 'increment', 'hello (world)1.1'], + ['hello world', 'world', 'decrement', 'hello (world)0.9'], + ] as const)('%s [%s] %s → %s', (prompt, selected, direction, expected) => { + expect(adjNumeric(prompt, selected, direction).prompt).toBe(expected); + }); + }); + + describe('successive numeric adjustments', () => { + it('should use additive step on second increment', () => { + const result = adjNumeric('(hello)1.1 world', '(hello)1.1', 'increment'); + expect(result.prompt).toBe('(hello)1.2 world'); + }); + + it('should use additive step on second decrement', () => { + const result = adjNumeric('(hello)0.9 world', '(hello)0.9', 'decrement'); + expect(result.prompt).toBe('(hello)0.8 world'); + }); + + it('should return to neutral from 1.1 on decrement', () => { + const result = adjNumeric('(hello)1.1 world', '(hello)1.1', 'decrement'); + expect(result.prompt).toBe('hello world'); + }); + }); + + describe('does not convert existing +/- attention on unselected terminals', () => { + it('should preserve +/- on unselected word when adjusting another', () => { + const result = adjNumeric('hello+ world', 'world', 'increment'); + expect(result.prompt).toContain('hello+'); + expect(result.prompt).toContain('(world)1.1'); + }); + + it('should preserve - on unselected word', () => { + const result = adjNumeric('hello- world', 'world', 'decrement'); + expect(result.prompt).toContain('hello-'); + expect(result.prompt).toContain('(world)0.9'); + }); + }); + + describe('existing +/- attention on selected terminals', () => { + it('should increment existing + word with multiplicative step (respects existing style)', () => { + const result = adjNumeric('hello+ world', 'hello+', 'increment'); + // The terminal already has explicit +/- attention, so it keeps that style + expect(result.prompt).toBe('hello++ world'); + }); + + it('should decrement existing + word to neutral', () => { + const result = adjNumeric('hello+ world', 'hello+', 'decrement'); + expect(result.prompt).toBe('hello world'); + }); + }); + + describe('existing numeric attention on selected terminals', () => { + it('should increment existing numeric weight additively', () => { + const result = adjNumeric('(detail)1.3 world', '(detail)1.3', 'increment'); + expect(result.prompt).toBe('(detail)1.4 world'); + }); + + it('should decrement existing numeric weight additively', () => { + const result = adjNumeric('(detail)1.3 world', '(detail)1.3', 'decrement'); + expect(result.prompt).toBe('(detail)1.2 world'); + }); + }); + + describe('multiple words selected', () => { + it('should wrap multiple words in numeric group on increment', () => { + const result = adjNumeric('hello world', [0, 11], 'increment'); + expect(result.prompt).toBe('(hello world)1.1'); + }); + + it('should wrap multiple words in numeric group on decrement', () => { + const result = adjNumeric('hello world', [0, 11], 'decrement'); + expect(result.prompt).toBe('(hello world)0.9'); + }); + }); + + describe('inside prompt functions', () => { + it('should use numeric format inside prompt function arg', () => { + const prompt = "('hello world', 'other').and()"; + const result = adjNumeric(prompt, 'hello', 'increment'); + expect(result.prompt).toBe("('(hello)1.1 world', 'other').and()"); + }); + + it('should use numeric format across prompt function separator', () => { + const prompt = "('one two', 'three four').and()"; + const start = prompt.indexOf('two'); + const end = prompt.indexOf('three') + 'three'.length; + const result = adjustPromptAttention(prompt, start, end, 'increment', true); + expect(result.prompt).toBe("('one (two)1.1', '(three)1.1 four').and()"); + }); + }); + + describe('group splitting inside prompt function args', () => { + it('should correctly split weighted group when decrementing a single word inside it', () => { + const prompt = + '("high detail, (cinematic lighting)1.25, soft volumetric light, (sharp focus)+, professional photography", "a young woman with balanced natural proportions, medium length brown hair, neutral expression, casual modern clothing", "subtle rim light, shallow depth of field, natural skin texture, clean background").and()'; + const result = adj(prompt, 'lighting', 'decrement'); + // "lighting" gets decremented from 1.25 → 1.25/1.1 ≈ 1.1364 + // "cinematic" stays at 1.25 + // The key thing: no space should be lost/misplaced + expect(result.prompt).toContain('(cinematic)1.25'); + expect(result.prompt).toContain('lighting)'); + // Verify there's a space between the cinematic group and lighting group + const cinIdx = result.prompt.indexOf('(cinematic)1.25'); + const afterCinematic = result.prompt.substring( + cinIdx + '(cinematic)1.25'.length, + cinIdx + '(cinematic)1.25'.length + 2 + ); + expect(afterCinematic).toMatch(/^ /); // Should start with a space + }); + + it('should rejoin groups when incrementing back to the same weight', () => { + const prompt = + '("high detail, (cinematic lighting)1.25, soft volumetric light, (sharp focus)+, professional photography", "a young woman with balanced natural proportions, medium length brown hair, neutral expression, casual modern clothing", "subtle rim light, shallow depth of field, natural skin texture, clean background").and()'; + // Decrement "lighting" to split the group + const step1 = adj(prompt, 'lighting', 'decrement'); + expect(step1.prompt).toContain('(cinematic)1.25'); + // Now increment "lighting" back — should rejoin into (cinematic lighting)1.25 + const step2 = adj(step1.prompt, 'lighting', 'increment'); + expect(step2.prompt).toContain('(cinematic lighting)1.25'); + }); + }); + + describe('numeric group whitespace trimming', () => { + it('should not capture trailing whitespace inside numeric weighted groups', () => { + // (foo bar)1.3 → decrement "bar" → (foo)1.3 (bar)X, with space between + const result = adj('(foo bar)1.3', 'bar', 'decrement'); + expect(result.prompt).toContain('(foo)1.3'); + // Space should be outside the group, not inside + expect(result.prompt).not.toContain('(foo )'); + expect(result.prompt).toMatch(/\(foo\)1\.3 /); + }); + + it('should not capture leading whitespace inside numeric weighted groups', () => { + // (foo bar)1.3 → decrement "foo" → (foo)X (bar)1.3, with space between + const result = adj('(foo bar)1.3', 'foo', 'decrement'); + expect(result.prompt).toContain('(bar)1.3'); + // Space should be outside the group, not inside + expect(result.prompt).not.toContain('( bar)'); + expect(result.prompt).toMatch(/ \(bar\)1\.3/); + }); + }); + + describe('numeric group conjoining', () => { + it('should merge adjacent same-weight numeric groups back together', () => { + // Two separate groups with same weight should conjoin into one + const result = adj('(foo)1.25 (bar)1.25', [0, 19], 'increment'); + // Both words get the same increment, so they should stay in one group + expect(result.prompt).not.toContain(') ('); + }); + + it('should merge adjacent same-weight groups when incrementing to match', () => { + // Start with (foo bar)1.3, decrement "bar", then increment it back + const step1 = adj('(foo bar)1.3', 'bar', 'decrement'); + // Now increment "bar" back — it should rejoin into a single group + const step2 = adj(step1.prompt, 'bar', 'increment'); + expect(step2.prompt).toBe('(foo bar)1.3'); + }); + + it('should merge inside prompt function args', () => { + const prompt = '("(cinematic)1.25 (lighting)1.25", "other").and()'; + const start = prompt.indexOf('cinematic'); + const end = prompt.indexOf('lighting') + 'lighting'.length; + const result = adj(prompt, [start, end], 'increment'); + // Both get incremented to same weight, should be one group + expect(result.prompt).not.toMatch(/\)\d[.\d]* \(/); + }); + }); + + describe('without prefersNumericWeights (default behavior unchanged)', () => { + it('should still use +/- syntax by default', () => { + expect(adj('hello world', 'hello', 'increment').prompt).toBe('hello+ world'); + expect(adj('hello world', 'hello', 'decrement').prompt).toBe('hello- world'); + }); + + it('should still use +/- for multiple words by default', () => { + expect(adj('hello world', [0, 11], 'increment').prompt).toBe('(hello world)+'); + }); + }); + }); +}); diff --git a/invokeai/frontend/web/src/common/util/promptAttention.ts b/invokeai/frontend/web/src/common/util/promptAttention.ts new file mode 100644 index 00000000000..baaafdb9d69 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/promptAttention.ts @@ -0,0 +1,638 @@ +import { logger } from 'app/logging/logger'; +import { serializeError } from 'serialize-error'; + +import { + type ASTNode, + type Attention, + parseTokens, + type PromptFunctionArg, + serializeWithSelection, + tokenize, +} from './promptAST'; + +const log = logger('generation'); + +type AttentionDirection = 'increment' | 'decrement'; +type AdjustmentResult = { prompt: string; selectionStart: number; selectionEnd: number }; + +const ATTENTION_STEP = 1.1; +const NUMERIC_ATTENTION_STEP = 0.1; + +/** Tolerance for floating-point weight comparisons. */ +const WEIGHT_TOLERANCE = 0.001; + +/** Tolerance for checking if a weight is a power of ATTENTION_STEP. */ +const STEP_COUNT_TOLERANCE = 0.005; + +// #region Weight Helpers + +/** + * Check if a weight is approximately ATTENTION_STEP^n for some integer n. + * Returns n if so, or null if the weight is not a power of ATTENTION_STEP. + */ +function getAttentionStepCount(weight: number): number | null { + if (weight <= 0) { + return null; + } + if (Math.abs(weight - 1.0) < WEIGHT_TOLERANCE) { + return 0; + } + const n = Math.round(Math.log(weight) / Math.log(ATTENTION_STEP)); + if (n === 0) { + return null; + } + const expected = Math.pow(ATTENTION_STEP, n); + if (Math.abs(expected - weight) < STEP_COUNT_TOLERANCE) { + return n; + } + return null; +} + +/** + * Convert an Attention value ('+', '--', 1.2, etc.) into a numeric multiplier. + */ +function parseAttention(attention: Attention): number { + if (typeof attention === 'number') { + return attention; + } + if (attention.startsWith('+')) { + return Math.pow(ATTENTION_STEP, attention.length); + } + if (attention.startsWith('-')) { + return Math.pow(ATTENTION_STEP, -attention.length); + } + const num = Number(attention); + return isNaN(num) ? 1.0 : num; +} + +/** + * Combine an existing attention value with an additional '+' or '-' level. + * Handles cancellation: e.g. '++' + '-' → '+', '+' + '-' → undefined (neutral). + */ +function addAttention(current: Attention | undefined, added: '+' | '-'): Attention | undefined { + if (!current) { + return added; + } + if (typeof current === 'number') { + if (added === '+') { + return Number((current * ATTENTION_STEP).toFixed(4)); + } + return Number((current / ATTENTION_STEP).toFixed(4)); + } + // Check if the added direction cancels the current one + const isCancel = (current.startsWith('+') && added === '-') || (current.startsWith('-') && added === '+'); + if (isCancel) { + const res = current.substring(1); + return res === '' ? undefined : res; + } + return `${current}${added}`; +} + +// #region Terminal Type + +type Terminal = { + text: string; + type: ASTNode['type']; + weight: number; + range: { start: number; end: number }; + hasExplicitAttention: boolean; + hasNumericAttention: boolean; + parentRange?: { start: number; end: number }; + isSelected: boolean; +}; + +// #region Main Entry Point + +/** + * Adjusts the attention of the prompt at the current cursor/selection position. + * Supports regular prompts and prompt functions (.and(), .or(), .blend()). + * + * When a selection spans across a prompt function's argument separator, each + * affected argument is adjusted independently and simultaneously. + */ +export function adjustPromptAttention( + prompt: string, + selectionStart: number, + selectionEnd: number, + direction: AttentionDirection, + prefersNumericWeights = false +): AdjustmentResult { + try { + const tokens = tokenize(prompt); + const ast = parseTokens(tokens); + + const regions = extractRegions(ast); + const processedNodes: ASTNode[] = []; + let anyModified = false; + + for (const region of regions) { + if (region.type === 'normal') { + const clipped = clipSelection(selectionStart, selectionEnd, region.range); + if (clipped) { + const result = adjustRegionNodes(region.nodes, clipped.start, clipped.end, direction, prefersNumericWeights); + if (result.modified) { + anyModified = true; + } + processedNodes.push(...result.nodes); + } else { + processedNodes.push(...region.nodes); + } + } else { + // prompt_function region + const pfNode = region.node; + const clipped = clipSelection(selectionStart, selectionEnd, pfNode.range); + if (clipped) { + const result = adjustPromptFunctionNode(pfNode, clipped.start, clipped.end, direction, prefersNumericWeights); + if (result.modified) { + anyModified = true; + } + processedNodes.push(result.node); + } else { + processedNodes.push(pfNode); + } + } + } + + if (!anyModified) { + return { prompt, selectionStart, selectionEnd }; + } + + return serializeWithSelection(processedNodes); + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + log.error({ error: serializeError(e) as any }, 'Failed to adjust prompt attention'); + return { prompt, selectionStart, selectionEnd }; + } +} + +// #region Region Extraction + +type Region = + | { type: 'normal'; nodes: ASTNode[]; range: { start: number; end: number } } + | { type: 'prompt_function'; node: ASTNode & { type: 'prompt_function' } }; + +/** + * Split the top-level AST into contiguous "normal" regions and prompt function regions. + * This allows us to process prompt function arguments independently. + */ +function extractRegions(ast: ASTNode[]): Region[] { + const regions: Region[] = []; + let currentNormal: ASTNode[] = []; + + const flushNormal = () => { + if (currentNormal.length > 0) { + const first = currentNormal[0]!; + const last = currentNormal[currentNormal.length - 1]!; + regions.push({ + type: 'normal', + nodes: currentNormal, + range: { start: first.range.start, end: last.range.end }, + }); + currentNormal = []; + } + }; + + for (const node of ast) { + if (node.type === 'prompt_function') { + flushNormal(); + regions.push({ type: 'prompt_function', node }); + } else { + currentNormal.push(node); + } + } + flushNormal(); + + return regions; +} + +/** + * Clip a selection range to a target range. Returns null if there is no overlap. + * For cursor positions (start === end), checks containment including boundaries. + */ +function clipSelection( + selStart: number, + selEnd: number, + range: { start: number; end: number } +): { start: number; end: number } | null { + if (selStart === selEnd) { + // Cursor position: check if within range (inclusive of boundaries) + if (selStart >= range.start && selStart <= range.end) { + return { start: selStart, end: selEnd }; + } + return null; + } + const clippedStart = Math.max(selStart, range.start); + const clippedEnd = Math.min(selEnd, range.end); + if (clippedStart >= clippedEnd) { + return null; + } + return { start: clippedStart, end: clippedEnd }; +} + +// #region Prompt Function Handling + +/** + * Adjust attention within a prompt function node by processing each argument + * whose content range overlaps the selection independently. + * Returns the (possibly updated) node and whether any modification was made. + */ +function adjustPromptFunctionNode( + pf: ASTNode & { type: 'prompt_function' }, + selStart: number, + selEnd: number, + direction: AttentionDirection, + prefersNumericWeights = false +): { node: ASTNode & { type: 'prompt_function' }; modified: boolean } { + let modified = false; + const newArgs: PromptFunctionArg[] = pf.promptArgs.map((arg) => { + const clipped = clipSelection(selStart, selEnd, arg.contentRange); + if (clipped) { + const result = adjustRegionNodes(arg.nodes, clipped.start, clipped.end, direction, prefersNumericWeights); + if (result.modified) { + modified = true; + return { ...arg, nodes: result.nodes }; + } + } + return arg; + }); + + if (!modified) { + return { node: pf, modified: false }; + } + + return { node: { ...pf, promptArgs: newArgs }, modified: true }; +} + +// #region Core Attention Adjustment + +/** + * Adjust attention for a set of AST nodes (a "region") given a selection range. + * This is the core flatten → select → adjust → regroup pipeline. + * Returns the adjusted nodes and whether any modification was made. + */ +function adjustRegionNodes( + nodes: ASTNode[], + selStart: number, + selEnd: number, + direction: AttentionDirection, + prefersNumericWeights = false +): { nodes: ASTNode[]; modified: boolean } { + const terminals = flattenAST(nodes); + + let selectedTerminals = selectTerminals(terminals, selStart, selEnd); + + // Fallback: if no terminals were selected, try to find an overlapping group + if (selectedTerminals.length === 0) { + const group = findSelectedGroup(nodes, selStart, selEnd); + if (group) { + selectedTerminals = terminals.filter((t) => t.range.start >= group.range.start && t.range.end <= group.range.end); + } + } + + if (selectedTerminals.length === 0) { + return { nodes, modified: false }; + } + + for (const t of selectedTerminals) { + t.isSelected = true; + // When the user prefers numeric weights and the terminal doesn't already + // have explicit attention, mark it as numeric so adjustWeights uses + // additive steps and groupTerminals emits numeric syntax. + if (prefersNumericWeights && !t.hasExplicitAttention) { + t.hasNumericAttention = true; + } + } + + adjustWeights(selectedTerminals, direction); + + return { nodes: groupTerminals(terminals), modified: true }; +} + +// #region Flatten AST to Terminals + +/** + * Flatten an AST into a flat list of terminals, computing the effective weight + * of each terminal by accumulating attention from ancestor groups. + */ +function flattenAST( + ast: ASTNode[], + currentWeight = 1.0, + parentRange?: { start: number; end: number }, + numericAttention = false +): Terminal[] { + const terminals: Terminal[] = []; + + for (const node of ast) { + let nodeWeight = currentWeight; + let nodeNumericAttention = numericAttention; + if ((node.type === 'word' || node.type === 'group') && node.attention) { + nodeWeight *= parseAttention(node.attention); + nodeNumericAttention = typeof node.attention === 'number'; + } + + if (node.type === 'group') { + terminals.push(...flattenAST(node.children, nodeWeight, node.range, nodeNumericAttention)); + } else if (node.type === 'prompt_function') { + // Prompt functions should not appear inside regions being flattened; + // they are handled at the region level. If one somehow appears, skip it. + continue; + } else { + terminals.push({ + text: node.type === 'word' ? node.text : node.value, + type: node.type, + weight: nodeWeight, + range: node.range, + hasExplicitAttention: node.type === 'word' && !!node.attention, + hasNumericAttention: nodeNumericAttention, + parentRange, + isSelected: false, + }); + } + } + return terminals; +} + +// #region Terminal Selection + +/** + * Find terminals that overlap the selection range and should be affected + * by the attention adjustment. Handles partial group overlap carefully: + * terminals with explicit attention inside partially-overlapping groups + * are excluded to avoid corrupting explicit weights. + * + * When the cursor is at a boundary between two tokens (e.g. "word|,"), + * both tokens technically overlap the cursor position. In this case we + * prefer word/embedding terminals over punctuation/whitespace so that + * adjusting attention at a word boundary doesn't accidentally include + * adjacent punctuation. + */ +function selectTerminals(terminals: Terminal[], selStart: number, selEnd: number): Terminal[] { + const result = terminals.filter((t) => { + const isOverlapping = + (t.range.start < selEnd && t.range.end > selStart) || + (selStart === selEnd && t.range.start <= selStart && t.range.end >= selStart); + + if (!isOverlapping) { + return false; + } + + if (t.parentRange) { + const parentContainsSelection = t.parentRange.start <= selStart && t.parentRange.end >= selEnd; + const selectionCoversParent = selStart <= t.parentRange.start && selEnd >= t.parentRange.end; + + if (!parentContainsSelection && !selectionCoversParent) { + // Partial overlap between selection and parent group + if (t.hasExplicitAttention) { + return false; // Don't modify explicit weight in partially-overlapping group + } + } + } + return true; + }); + + // When the cursor is at a token boundary (no selection range), multiple tokens + // can match. Prefer word/embedding terminals over punctuation/whitespace. + if (selStart === selEnd && result.length > 1) { + const contentTerminals = result.filter((t) => t.type === 'word' || t.type === 'embedding'); + if (contentTerminals.length > 0) { + return contentTerminals; + } + } + + return result; +} + +// #region Weight Adjustment + +/** + * Apply weight changes to the selected terminals based on direction. + * Numeric weights use additive steps; +/- syntax uses multiplicative steps. + * All results are rounded to 4 decimal places to prevent floating-point drift. + */ +function adjustWeights(terminals: Terminal[], direction: AttentionDirection): void { + for (const terminal of terminals) { + if (terminal.hasNumericAttention) { + // Additive step for explicit numeric weights (e.g. 1.1 → 1.2) + if (direction === 'increment') { + terminal.weight = Number((terminal.weight + NUMERIC_ATTENTION_STEP).toFixed(4)); + } else { + terminal.weight = Number((terminal.weight - NUMERIC_ATTENTION_STEP).toFixed(4)); + } + } else { + // Multiplicative step for +/- syntax weights, rounded to prevent drift + if (direction === 'increment') { + terminal.weight = Number((terminal.weight * ATTENTION_STEP).toFixed(4)); + } else { + terminal.weight = Number((terminal.weight / ATTENTION_STEP).toFixed(4)); + } + } + } +} + +// #region Find Selected Group (fallback) + +/** + * When no terminals directly overlap the selection (e.g. cursor is on a group + * boundary character), find the innermost group that overlaps the selection. + */ +function findSelectedGroup(nodes: ASTNode[], start: number, end: number): ASTNode | null { + for (const node of nodes) { + if (node.type === 'group') { + const foundInChildren = findSelectedGroup(node.children, start, end); + if (foundInChildren) { + return foundInChildren; + } + if (node.range.start < end && node.range.end > start) { + return node; + } + } + } + return null; +} + +// #region Regroup Terminals into AST + +/** + * Reconstruct an AST from a flat list of terminals with adjusted weights. + * Groups consecutive terminals with compatible weights using +/- or numeric syntax. + * + * Note: Reconstructed group nodes use `range: { start: 0, end: 0 }` as a sentinel + * value since the original source positions are no longer meaningful after regrouping. + * These nodes are only used for serialization output, never for source-position lookups. + */ +function groupTerminals(terminals: Terminal[]): ASTNode[] { + if (terminals.length === 0) { + return []; + } + + /** Sentinel range for reconstructed nodes whose original positions are not applicable. */ + const NO_RANGE = { start: 0, end: 0 }; + + const nodes: ASTNode[] = []; + let i = 0; + + while (i < terminals.length) { + const t = terminals[i]!; + const weight = t.weight; + const stepCount = getAttentionStepCount(weight); + + // ── +/- attention (weight is a non-zero power of ATTENTION_STEP) ── + // Skip this branch if the terminal prefers numeric format to avoid an + // infinite loop (predicate would reject it, findRunEnd returns i, i never advances). + if (stepCount !== null && stepCount !== 0 && !t.hasNumericAttention) { + const isPositive = stepCount > 0; + const sign: '+' | '-' = isPositive ? '+' : '-'; + const predicate = (t: Terminal): boolean => { + if (t.hasNumericAttention) { + return false; // Numeric-preference terminals should not join +/- runs + } + const sc = getAttentionStepCount(t.weight); + return sc !== null && (isPositive ? sc > 0 : sc < 0); + }; + const factor = isPositive ? ATTENTION_STEP : 1 / ATTENTION_STEP; + + const j = findRunEnd(terminals, i, predicate); + + // Trim whitespace from the content run boundaries + let runStart = i; + let runEnd = j; + while (runStart < runEnd && terminals[runStart]!.type === 'whitespace') { + runStart++; + } + while (runEnd > runStart && terminals[runEnd - 1]!.type === 'whitespace') { + runEnd--; + } + + // Emit leading whitespace as standalone nodes + for (let k = i; k < runStart; k++) { + nodes.push(createNodeFromTerminal(terminals[k]!)); + } + + if (runStart < runEnd) { + // Factor out one level of attention and recurse + const slice = terminals.slice(runStart, runEnd).map((t) => ({ ...t, weight: t.weight / factor })); + const children = groupTerminals(slice); + const isSelection = slice.every((t) => t.isSelected); + + if (children.length === 1) { + const child = children[0]!; + if (child.type === 'word' || child.type === 'group') { + const newAttention = addAttention(child.attention, sign); + nodes.push({ ...child, attention: newAttention, isSelection: isSelection || undefined }); + } else { + nodes.push({ type: 'group', children, attention: sign, range: NO_RANGE, isSelection }); + } + } else { + nodes.push({ type: 'group', children, attention: sign, range: NO_RANGE, isSelection }); + } + } + + // Emit trailing whitespace as standalone nodes + for (let k = runEnd; k < j; k++) { + nodes.push(createNodeFromTerminal(terminals[k]!)); + } + + i = j; + continue; + } + + // ── Neutral weight (≈ 1.0) ── + if (Math.abs(weight - 1.0) < WEIGHT_TOLERANCE) { + nodes.push(createNodeFromTerminal(t)); + i++; + continue; + } + + // ── Numeric weight (not a power of ATTENTION_STEP) ── + { + const j = findRunEnd(terminals, i, (t) => Math.abs(t.weight - weight) < WEIGHT_TOLERANCE); + + // Trim whitespace from the content run boundaries (same as +/- branch) + let runStart = i; + let runEnd = j; + while (runStart < runEnd && terminals[runStart]!.type === 'whitespace') { + runStart++; + } + while (runEnd > runStart && terminals[runEnd - 1]!.type === 'whitespace') { + runEnd--; + } + + // Emit leading whitespace as standalone nodes + for (let k = i; k < runStart; k++) { + nodes.push(createNodeFromTerminal(terminals[k]!)); + } + + if (runStart < runEnd) { + const groupSlice = terminals.slice(runStart, runEnd).map((t) => ({ ...t, weight: 1.0 })); + const children = groupTerminals(groupSlice); + const isSelection = groupSlice.every((t) => t.isSelected); + const weightNum = Number(weight.toFixed(4)); + + nodes.push({ type: 'group', children, attention: weightNum, range: NO_RANGE, isSelection }); + } + + // Emit trailing whitespace as standalone nodes + for (let k = runEnd; k < j; k++) { + nodes.push(createNodeFromTerminal(terminals[k]!)); + } + + i = j; + } + } + return nodes; +} + +/** + * Find the end of a "run" of terminals whose weights satisfy a predicate. + * Whitespace terminals are included if the next non-whitespace terminal also satisfies the predicate. + * Note: The returned index may point to a whitespace token that is NOT included in the run; + * the caller is responsible for trimming trailing whitespace from the run boundaries. + */ +function findRunEnd(terminals: Terminal[], start: number, predicate: (t: Terminal) => boolean): number { + let j = start; + while (j < terminals.length) { + const next = terminals[j]!; + if (predicate(next)) { + j++; + } else if (next.type === 'whitespace') { + // Look ahead past consecutive whitespace + let k = j + 1; + while (k < terminals.length && terminals[k]!.type === 'whitespace') { + k++; + } + if (k < terminals.length && predicate(terminals[k]!)) { + j = k; + } else { + break; + } + } else { + break; + } + } + return j; +} + +/** + * Convert a Terminal back into a leaf ASTNode. + */ +function createNodeFromTerminal(t: Terminal): ASTNode { + switch (t.type) { + case 'word': + return { type: 'word', text: t.text, range: t.range, isSelection: t.isSelected || undefined }; + case 'whitespace': + return { type: 'whitespace', value: t.text, range: t.range, isSelection: t.isSelected || undefined }; + case 'punct': + return { type: 'punct', value: t.text, range: t.range, isSelection: t.isSelected || undefined }; + case 'embedding': + return { type: 'embedding', value: t.text, range: t.range, isSelection: t.isSelected || undefined }; + case 'escaped_paren': + return { + type: 'escaped_paren', + value: t.text as '(' | ')', + range: t.range, + isSelection: t.isSelected || undefined, + }; + default: + return { type: 'word', text: t.text, range: t.range, isSelection: t.isSelected || undefined }; + } +} diff --git a/invokeai/frontend/web/src/common/util/randomFloat.ts b/invokeai/frontend/web/src/common/util/randomFloat.ts new file mode 100644 index 00000000000..084eede6bc8 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/randomFloat.ts @@ -0,0 +1,5 @@ +const randomFloat = (min: number, max: number): number => { + return Math.random() * (max - min + Number.EPSILON) + min; +}; + +export default randomFloat; diff --git a/invokeai/frontend/web/src/common/util/randomInt.ts b/invokeai/frontend/web/src/common/util/randomInt.ts new file mode 100644 index 00000000000..078186f3da4 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/randomInt.ts @@ -0,0 +1,5 @@ +const randomInt = (min: number, max: number): number => { + return Math.floor(Math.random() * (max - min + 1) + min); +}; + +export default randomInt; diff --git a/invokeai/frontend/web/src/common/util/result.test.ts b/invokeai/frontend/web/src/common/util/result.test.ts new file mode 100644 index 00000000000..4403289a1e7 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/result.test.ts @@ -0,0 +1,73 @@ +import type { Equals } from 'tsafe'; +import { assert } from 'tsafe'; +import { describe, expect, it } from 'vitest'; + +import { Err, ErrResult, Ok, OkResult, withResult, withResultAsync } from './result'; + +const promiseify = (fn: () => T): (() => Promise) => { + return () => + new Promise((resolve) => { + resolve(fn()); + }); +}; + +describe('Result Utility Functions', () => { + it('OkResult() should create an Ok result', () => { + const result = OkResult(42); + expect(result).toBeInstanceOf(Ok); + expect(result.isOk()).toBe(true); + expect(result.isErr()).toBe(false); + expect(result.value).toBe(42); + assert, typeof result>>(result); + }); + + it('ErrResult() should create an Err result', () => { + const error = new Error('Something went wrong'); + const result = ErrResult(error); + expect(result).toBeInstanceOf(Err); + expect(result.isOk()).toBe(false); + expect(result.isErr()).toBe(true); + expect(result.error).toBe(error); + assert, typeof result>>(result); + }); + + it('withResult() should return Ok on success', () => { + const fn = () => 42; + const result = withResult(fn); + expect(result.isOk()).toBe(true); + if (result.isOk()) { + expect(result.value).toBe(42); + } + }); + + it('withResult() should return Err on exception', () => { + const fn = () => { + throw new Error('Failure'); + }; + const result = withResult(fn); + expect(result.isErr()).toBe(true); + if (result.isErr()) { + expect(result.error.message).toBe('Failure'); + } + }); + + it('withResultAsync() should return Ok on success', async () => { + const fn = promiseify(() => 42); + const result = await withResultAsync(fn); + expect(result.isOk()).toBe(true); + if (result.isOk()) { + expect(result.value).toBe(42); + } + }); + + it('withResultAsync() should return Err on exception', async () => { + const fn = promiseify(() => { + throw new Error('Async failure'); + }); + const result = await withResultAsync(fn); + expect(result.isErr()).toBe(true); + if (result.isErr()) { + expect(result.error.message).toBe('Async failure'); + } + }); +}); diff --git a/invokeai/frontend/web/src/common/util/result.ts b/invokeai/frontend/web/src/common/util/result.ts new file mode 100644 index 00000000000..4dc801d9372 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/result.ts @@ -0,0 +1,126 @@ +/** + * Represents a successful result. + * @template T The type of the value. + */ +export class Ok { + readonly value: T; + constructor(value: T) { + this.value = value; + } + + /** + * Type guard to check if this result is an `Ok` result. + * @returns {this is Ok} `true` if the result is an `Ok` result, otherwise `false`. + */ + isOk(): this is Ok { + return true; + } + + /** + * Type guard to check if this result is an `Err` result. + * @returns {this is Err} `true` if the result is an `Err` result, otherwise `false`. + */ + isErr(): this is Err { + return false; + } +} + +/** + * Represents a failed result. + * @template E The type of the error. + */ +export class Err { + readonly error: E; + constructor(error: E) { + this.error = error; + } + + /** + * Type guard to check if this result is an `Ok` result. + * @returns {this is Ok} `true` if the result is an `Ok` result, otherwise `false`. + */ + isOk(): this is Ok { + return false; + } + + /** + * Type guard to check if this result is an `Err` result. + * @returns {this is Err} `true` if the result is an `Err` result, otherwise `false`. + */ + isErr(): this is Err { + return true; + } +} + +/** + * A union type that represents either a successful result (`Ok`) or a failed result (`Err`). + * @template T The type of the value in the `Ok` case. + * @template E The type of the error in the `Err` case. + */ +type Result = Ok | Err; + +/** + * Creates a successful result. + * @template T The type of the value. + * @param {T} value The value to wrap in an `Ok` result. + * @returns {Ok} The `Ok` result containing the value. + */ +export function OkResult(value: T): Ok { + return new Ok(value); +} + +/** + * Creates a failed result. + * @template E The type of the error. + * @param {E} error The error to wrap in an `Err` result. + * @returns {Err} The `Err` result containing the error. + */ +export function ErrResult(error: E): Err { + return new Err(error); +} + +/** + * Wraps a synchronous function in a try-catch block, returning a `Result`. + * @template T The type of the value returned by the function. + * @param {() => T} fn The function to execute. + * @returns {Result} An `Ok` result if the function succeeds, or an `Err` result if it throws an error. + */ +export function withResult(fn: () => T): Result { + try { + return new Ok(fn()); + } catch (error) { + return new Err(error instanceof Error ? error : new WrappedError(error)); + } +} + +/** + * Wraps an asynchronous function in a try-catch block, returning a `Promise` of a `Result`. + * @template T The type of the value returned by the function. + * @param {() => Promise} fn The asynchronous function to execute. + * @returns {Promise>} A `Promise` resolving to an `Ok` result if the function succeeds, or an `Err` result if it throws an error. + */ +export async function withResultAsync(fn: () => Promise): Promise> { + try { + const result = await fn(); + return new Ok(result); + } catch (error) { + return new Err(error instanceof Error ? error : new WrappedError(error)); + } +} + +export class WrappedError extends Error { + error: unknown; + + constructor(error: unknown) { + super('Wrapped Error'); + this.name = this.constructor.name; + this.error = error; + } + + static wrap(error: unknown): Error | WrappedError { + if (error instanceof Error) { + return error; + } + return new WrappedError(error); + } +} diff --git a/invokeai/frontend/web/src/common/util/roundDownToMultiple.ts b/invokeai/frontend/web/src/common/util/roundDownToMultiple.ts new file mode 100644 index 00000000000..ce47effe406 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/roundDownToMultiple.ts @@ -0,0 +1,14 @@ +export const roundDownToMultiple = (num: number, multiple: number): number => { + return Math.floor(num / multiple) * multiple; +}; +export const roundUpToMultiple = (num: number, multiple: number): number => { + return Math.ceil(num / multiple) * multiple; +}; + +export const roundToMultiple = (num: number, multiple: number): number => { + return Math.round(num / multiple) * multiple; +}; + +export const roundToMultipleMin = (num: number, multiple: number): number => { + return Math.max(Math.round(num / multiple) * multiple, multiple); +}; diff --git a/invokeai/frontend/web/src/common/util/serialize.ts b/invokeai/frontend/web/src/common/util/serialize.ts new file mode 100644 index 00000000000..a5db921f8db --- /dev/null +++ b/invokeai/frontend/web/src/common/util/serialize.ts @@ -0,0 +1,10 @@ +/** + * Serialize an object to JSON and back to a new object + */ +export const parseify = (obj: unknown) => { + try { + return JSON.parse(JSON.stringify(obj)); + } catch { + return 'Error parsing object'; + } +}; diff --git a/invokeai/frontend/web/src/common/util/stopPropagation.ts b/invokeai/frontend/web/src/common/util/stopPropagation.ts new file mode 100644 index 00000000000..a4bafe68cf9 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/stopPropagation.ts @@ -0,0 +1,5 @@ +import type { MouseEvent } from 'react'; + +export const preventDefault = (e: MouseEvent) => { + e.preventDefault(); +}; diff --git a/invokeai/frontend/web/src/common/util/typedMemo.ts b/invokeai/frontend/web/src/common/util/typedMemo.ts new file mode 100644 index 00000000000..adf3473de04 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/typedMemo.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type React from 'react'; +import { memo } from 'react'; + +/** + * A typed version of React.memo, useful for components that take generics. + */ +export const typedMemo: >( + component: T, + propsAreEqual?: (prevProps: React.ComponentProps, nextProps: React.ComponentProps) => boolean +) => T & { displayName?: string } = memo; diff --git a/invokeai/frontend/web/src/common/util/zodUtils.ts b/invokeai/frontend/web/src/common/util/zodUtils.ts new file mode 100644 index 00000000000..10506736e18 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/zodUtils.ts @@ -0,0 +1,10 @@ +import type { z } from 'zod'; + +/** + * Helper to create a type guard from a zod schema. The type guard will infer the schema's TS type. + * @param schema The zod schema to create a type guard from. + * @returns A type guard function for the schema. + */ +export const buildZodTypeGuard = (schema: T) => { + return (val: unknown): val is z.infer => schema.safeParse(val).success; +}; diff --git a/invokeai/frontend/web/src/features/auth/components/AdministratorSetup.tsx b/invokeai/frontend/web/src/features/auth/components/AdministratorSetup.tsx new file mode 100644 index 00000000000..b0ad9a5e047 --- /dev/null +++ b/invokeai/frontend/web/src/features/auth/components/AdministratorSetup.tsx @@ -0,0 +1,242 @@ +import { + Box, + Button, + Center, + Flex, + FormControl, + FormErrorMessage, + FormHelperText, + FormLabel, + Grid, + GridItem, + Heading, + Input, + Spinner, + Text, + VStack, +} from '@invoke-ai/ui-library'; +import { validatePasswordField } from 'features/auth/util/passwordUtils'; +import type { ChangeEvent, FormEvent } from 'react'; +import { memo, useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { useGetSetupStatusQuery, useSetupMutation } from 'services/api/endpoints/auth'; + +export const AdministratorSetup = memo(() => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const [email, setEmail] = useState(''); + const [displayName, setDisplayName] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [setup, { isLoading, error }] = useSetupMutation(); + const { data: setupStatus, isLoading: isLoadingSetup } = useGetSetupStatusQuery(); + + // Redirect to app if multiuser mode is disabled + useEffect(() => { + if (!isLoadingSetup && setupStatus && !setupStatus.multiuser_enabled) { + navigate('/app', { replace: true }); + } + }, [setupStatus, isLoadingSetup, navigate]); + + const strictPasswordChecking = setupStatus?.strict_password_checking ?? true; + const passwordValidation = validatePasswordField(password, t, strictPasswordChecking, false); + const passwordsMatch = password === confirmPassword; + + const handleSubmit = useCallback( + async (e: FormEvent) => { + e.preventDefault(); + + if (!passwordValidation.isValid) { + return; + } + + if (!passwordsMatch) { + return; + } + + try { + const result = await setup({ email, display_name: displayName, password }).unwrap(); + if (result.success) { + // Auto-login after setup - need to call login API + // For now, just redirect to login page + window.location.href = '/login'; + } + } catch { + // Error is handled by RTK Query and displayed via error state + } + }, + [email, displayName, password, passwordValidation.isValid, passwordsMatch, setup] + ); + + const handleEmailChange = useCallback((e: ChangeEvent) => { + setEmail(e.target.value); + }, []); + + const handleDisplayNameChange = useCallback((e: ChangeEvent) => { + setDisplayName(e.target.value); + }, []); + + const handlePasswordChange = useCallback((e: ChangeEvent) => { + setPassword(e.target.value); + }, []); + + const handleConfirmPasswordChange = useCallback((e: ChangeEvent) => { + setConfirmPassword(e.target.value); + }, []); + + const errorMessage = error + ? 'data' in error && typeof error.data === 'object' && error.data && 'detail' in error.data + ? String(error.data.detail) + : t('auth.setup.setupFailed') + : null; + + // Show loading spinner while checking setup status or redirecting + if (isLoadingSetup || (setupStatus && !setupStatus.multiuser_enabled)) { + return ( +
+ +
+ ); + } + + const passwordStrengthColor = + passwordValidation.strength === 'weak' + ? 'error.300' + : passwordValidation.strength === 'moderate' + ? 'warning.300' + : 'invokeBlue.300'; + + return ( +
+ +
+ + + + {t('auth.setup.title')} + + + {t('auth.setup.subtitle')} + + + + + + + + {t('auth.setup.email')} + + + + + {t('auth.setup.emailHelper')} + + + + + + + + + {t('auth.setup.displayName')} + + + + + {t('auth.setup.displayNameHelper')} + + + + + 0 && !passwordValidation.isValid}> + + + + {t('auth.setup.password')} + + + + + {password.length > 0 && !passwordValidation.isValid && ( + {passwordValidation.message} + )} + {password.length > 0 && passwordValidation.isValid && passwordValidation.message && ( + + {passwordValidation.message} + + )} + {password.length === 0 && ( + + {strictPasswordChecking ? t('auth.setup.passwordHelper') : t('auth.setup.passwordHelperRelaxed')} + + )} + + + + + 0 && !passwordsMatch}> + + + + {t('auth.setup.confirmPassword')} + + + + + {confirmPassword.length > 0 && !passwordsMatch && ( + {t('auth.setup.passwordsDoNotMatch')} + )} + + + + + + + {errorMessage && ( + + {errorMessage} + + )} + +
+
+
+ ); +}); + +AdministratorSetup.displayName = 'AdministratorSetup'; diff --git a/invokeai/frontend/web/src/features/auth/components/LoginPage.tsx b/invokeai/frontend/web/src/features/auth/components/LoginPage.tsx new file mode 100644 index 00000000000..b4f01d5878b --- /dev/null +++ b/invokeai/frontend/web/src/features/auth/components/LoginPage.tsx @@ -0,0 +1,175 @@ +import { + Box, + Button, + Center, + Checkbox, + Flex, + FormControl, + FormErrorMessage, + FormLabel, + Heading, + Input, + Spinner, + Text, + VStack, +} from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { selectSessionExpired, setCredentials } from 'features/auth/store/authSlice'; +import type { ChangeEvent, FormEvent } from 'react'; +import { memo, useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { useGetSetupStatusQuery, useLoginMutation } from 'services/api/endpoints/auth'; + +export const LoginPage = memo(() => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [rememberMe, setRememberMe] = useState(true); + const [login, { isLoading, error }] = useLoginMutation(); + const dispatch = useAppDispatch(); + const sessionExpired = useAppSelector(selectSessionExpired); + const { data: setupStatus, isLoading: isLoadingSetup } = useGetSetupStatusQuery(); + + // Redirect to app if multiuser mode is disabled + useEffect(() => { + if (!isLoadingSetup && setupStatus && !setupStatus.multiuser_enabled) { + navigate('/app', { replace: true }); + } + }, [setupStatus, isLoadingSetup, navigate]); + + // Redirect to setup page if setup is required + useEffect(() => { + if (!isLoadingSetup && setupStatus?.setup_required) { + navigate('/setup', { replace: true }); + } + }, [setupStatus, isLoadingSetup, navigate]); + + const handleSubmit = useCallback( + async (e: FormEvent) => { + e.preventDefault(); + try { + const result = await login({ email, password, remember_me: rememberMe }).unwrap(); + // Map the UserDTO from API to our User type + const user = { + user_id: result.user.user_id, + email: result.user.email, + display_name: result.user.display_name || null, + is_admin: result.user.is_admin || false, + is_active: result.user.is_active || true, + }; + dispatch(setCredentials({ token: result.token, user })); + // Force a page reload to ensure all user-specific state is loaded from server + // This is important for multiuser isolation to prevent state leakage + window.location.href = '/app'; + } catch { + // Error is handled by RTK Query and displayed via error state + } + }, + [email, password, rememberMe, login, dispatch] + ); + + const handleEmailChange = useCallback((e: ChangeEvent) => { + setEmail(e.target.value); + }, []); + + const handlePasswordChange = useCallback((e: ChangeEvent) => { + setPassword(e.target.value); + }, []); + + const handleRememberMeChange = useCallback((e: ChangeEvent) => { + setRememberMe(e.target.checked); + }, []); + + const errorMessage = error + ? 'data' in error && typeof error.data === 'object' && error.data && 'detail' in error.data + ? String(error.data.detail) + : t('auth.login.loginFailed') + : null; + + // Show loading spinner while checking setup status or redirecting + if (isLoadingSetup || (setupStatus && !setupStatus.multiuser_enabled)) { + return ( +
+ +
+ ); + } + + // Show loading spinner if setup is required (redirecting to setup) + if (setupStatus?.setup_required) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ + + {t('auth.login.title')} + + + {sessionExpired && ( + + {t('auth.login.sessionExpired')} + + )} + + + {t('auth.login.email')} + + + + + {t('auth.login.password')} + + {errorMessage && {errorMessage}} + + + + {t('auth.login.rememberMe')} + + + + + {errorMessage && ( + + {errorMessage} + + )} + +
+
+
+ ); +}); + +LoginPage.displayName = 'LoginPage'; diff --git a/invokeai/frontend/web/src/features/auth/components/ProtectedRoute.tsx b/invokeai/frontend/web/src/features/auth/components/ProtectedRoute.tsx new file mode 100644 index 00000000000..82752523050 --- /dev/null +++ b/invokeai/frontend/web/src/features/auth/components/ProtectedRoute.tsx @@ -0,0 +1,129 @@ +import { Center, Spinner } from '@invoke-ai/ui-library'; +import type { RootState } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { logout, sessionExpiredLogout, setCredentials } from 'features/auth/store/authSlice'; +import type { PropsWithChildren } from 'react'; +import { memo, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useGetCurrentUserQuery, useGetSetupStatusQuery } from 'services/api/endpoints/auth'; + +interface ProtectedRouteProps { + requireAdmin?: boolean; +} + +export const ProtectedRoute = memo(({ children, requireAdmin = false }: PropsWithChildren) => { + const isAuthenticated = useAppSelector((state: RootState) => state.auth?.isAuthenticated || false); + const token = useAppSelector((state: RootState) => state.auth?.token); + const user = useAppSelector((state: RootState) => state.auth?.user); + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + + // Check if multiuser mode is enabled + const { data: setupStatus } = useGetSetupStatusQuery(); + const multiuserEnabled = setupStatus?.multiuser_enabled ?? true; // Default to true for safety + + // Only fetch user if we have a token but no user data, and multiuser mode is enabled + const shouldFetchUser = multiuserEnabled && isAuthenticated && token && !user; + const { + data: currentUser, + isLoading: isLoadingUser, + error: userError, + } = useGetCurrentUserQuery(undefined, { + skip: !shouldFetchUser, + }); + + useEffect(() => { + // Only treat 401 as session expiry. Other errors (500, network, etc.) are + // transient and should not force logout — the 401 handler in dynamicBaseQuery + // already covers the actual expiry case. + if (userError && isAuthenticated && 'status' in userError && userError.status === 401) { + dispatch(sessionExpiredLogout()); + navigate('/login', { replace: true }); + } + }, [userError, isAuthenticated, dispatch, navigate]); + + // Detect when auth_token is removed from localStorage (e.g. by another tab, + // browser devtools, or token expiry cleanup). The 'storage' event fires when + // localStorage is modified by another context; we also poll periodically to + // catch same-tab deletions (which don't trigger the storage event). + useEffect(() => { + if (!multiuserEnabled || !isAuthenticated) { + return; + } + + const checkToken = () => { + if (!localStorage.getItem('auth_token') && isAuthenticated) { + dispatch(sessionExpiredLogout()); + navigate('/login', { replace: true }); + } + }; + + // Listen for cross-tab localStorage changes + window.addEventListener('storage', checkToken); + // Poll for same-tab deletions (e.g. browser console) + const interval = setInterval(checkToken, 5000); + + return () => { + window.removeEventListener('storage', checkToken); + clearInterval(interval); + }; + }, [multiuserEnabled, isAuthenticated, dispatch, navigate]); + + useEffect(() => { + // If we successfully fetched user data, update auth state + if (currentUser && token && !user) { + const userObj = { + user_id: currentUser.user_id, + email: currentUser.email, + display_name: currentUser.display_name || null, + is_admin: currentUser.is_admin || false, + is_active: currentUser.is_active || true, + }; + dispatch(setCredentials({ token, user: userObj })); + } + }, [currentUser, token, user, dispatch]); + + useEffect(() => { + // If multiuser is disabled, allow access without authentication + if (!multiuserEnabled) { + // Clear any persisted auth state when switching to single-user mode + if (isAuthenticated) { + dispatch(logout()); + } + return; + } + + // In multiuser mode, check authentication + if (!isLoadingUser && !isAuthenticated) { + navigate('/login', { replace: true }); + } else if (!isLoadingUser && isAuthenticated && user && requireAdmin && !user.is_admin) { + navigate('/', { replace: true }); + } + }, [isAuthenticated, isLoadingUser, requireAdmin, user, navigate, multiuserEnabled, dispatch]); + + // In single-user mode, always allow access + if (!multiuserEnabled) { + return <>{children}; + } + + // Show loading while fetching user data + if (isLoadingUser || (isAuthenticated && !user)) { + return ( +
+ +
+ ); + } + + if (!isAuthenticated) { + return null; + } + + if (requireAdmin && !user?.is_admin) { + return null; + } + + return <>{children}; +}); + +ProtectedRoute.displayName = 'ProtectedRoute'; diff --git a/invokeai/frontend/web/src/features/auth/components/UserManagement.tsx b/invokeai/frontend/web/src/features/auth/components/UserManagement.tsx new file mode 100644 index 00000000000..8d587e7249e --- /dev/null +++ b/invokeai/frontend/web/src/features/auth/components/UserManagement.tsx @@ -0,0 +1,641 @@ +import { + Badge, + Box, + Button, + Center, + Checkbox, + Flex, + FormControl, + FormErrorMessage, + FormLabel, + Grid, + GridItem, + Heading, + IconButton, + Input, + InputGroup, + InputRightElement, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Spinner, + Switch, + Table, + Tbody, + Td, + Text, + Th, + Thead, + Tooltip, + Tr, + useDisclosure, + VStack, +} from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCurrentUser } from 'features/auth/store/authSlice'; +import { validatePasswordField } from 'features/auth/util/passwordUtils'; +import type { ChangeEvent, FormEvent } from 'react'; +import { memo, useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + PiArrowLeftBold, + PiEyeBold, + PiEyeSlashBold, + PiLightningFill, + PiPencilBold, + PiPlusBold, + PiTrashBold, +} from 'react-icons/pi'; +import { useNavigate } from 'react-router-dom'; +import type { UserDTO } from 'services/api/endpoints/auth'; +import { + useCreateUserMutation, + useDeleteUserMutation, + useGetSetupStatusQuery, + useLazyGeneratePasswordQuery, + useListUsersQuery, + useUpdateUserMutation, +} from 'services/api/endpoints/auth'; + +const FORM_GRID_COLUMNS = '120px 1fr'; + +// --------------------------------------------------------------------------- +// Create / Edit user modal +// --------------------------------------------------------------------------- + +type UserFormModalProps = { + isOpen: boolean; + onClose: () => void; + /** When provided, the modal operates in "edit" mode for the given user */ + editUser?: UserDTO | null; +}; + +const UserFormModal = memo(({ isOpen, onClose, editUser }: UserFormModalProps) => { + const { t } = useTranslation(); + const isEdit = !!editUser; + + const [email, setEmail] = useState(editUser?.email ?? ''); + const [displayName, setDisplayName] = useState(editUser?.display_name ?? ''); + const [password, setPassword] = useState(''); + const [isAdmin, setIsAdmin] = useState(editUser?.is_admin ?? false); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(null); + + const [createUser, { isLoading: isCreating }] = useCreateUserMutation(); + const [updateUser, { isLoading: isUpdating }] = useUpdateUserMutation(); + const [triggerGeneratePassword] = useLazyGeneratePasswordQuery(); + const { data: setupStatus } = useGetSetupStatusQuery(); + + const isLoading = isCreating || isUpdating; + const strictPasswordChecking = setupStatus?.strict_password_checking ?? true; + // In edit mode, empty password means "no change" (allowEmpty=true); in create mode password is required (allowEmpty=false) + const passwordValidation = validatePasswordField(password, t, strictPasswordChecking, isEdit); + + const handleGeneratePassword = useCallback(async () => { + try { + const result = await triggerGeneratePassword().unwrap(); + setPassword(result.password); + setShowPassword(true); + } catch { + // ignore + } + }, [triggerGeneratePassword]); + + const toggleShowPassword = useCallback(() => { + setShowPassword((v) => !v); + }, []); + + const handleEmailChange = useCallback((e: ChangeEvent) => { + setEmail(e.target.value); + }, []); + + const handleDisplayNameChange = useCallback((e: ChangeEvent) => { + setDisplayName(e.target.value); + }, []); + + const handlePasswordChange = useCallback((e: ChangeEvent) => { + setPassword(e.target.value); + }, []); + + const handleIsAdminChange = useCallback((e: ChangeEvent) => { + setIsAdmin(e.target.checked); + }, []); + + const handleSubmit = useCallback( + async (e: FormEvent) => { + e.preventDefault(); + setError(null); + + if (!isEdit && (!password || !passwordValidation.isValid)) { + return; + } + if (isEdit && password && !passwordValidation.isValid) { + return; + } + + try { + if (isEdit && editUser) { + const updateData: Parameters[0]['data'] = { + display_name: displayName || null, + is_admin: isAdmin, + }; + if (password) { + updateData.password = password; + } + await updateUser({ + userId: editUser.user_id, + data: updateData, + }).unwrap(); + } else { + await createUser({ + email, + display_name: displayName || null, + password, + is_admin: isAdmin, + }).unwrap(); + } + onClose(); + } catch (err) { + const detail = + err && typeof err === 'object' && 'data' in err && typeof (err as { data: unknown }).data === 'object' + ? ((err as { data: { detail?: string } }).data?.detail ?? t('auth.userManagement.saveFailed')) + : t('auth.userManagement.saveFailed'); + setError(detail); + } + }, + [ + isEdit, + editUser, + email, + displayName, + password, + isAdmin, + passwordValidation.isValid, + createUser, + updateUser, + onClose, + t, + ] + ); + + // Reset local state when modal closes + const handleClose = useCallback(() => { + setEmail(editUser?.email ?? ''); + setDisplayName(editUser?.display_name ?? ''); + setPassword(''); + setIsAdmin(editUser?.is_admin ?? false); + setShowPassword(false); + setError(null); + onClose(); + }, [editUser, onClose]); + + return ( + + + +
+ {isEdit ? t('auth.userManagement.editUser') : t('auth.userManagement.createUser')} + + + + {!isEdit && ( + + + + + {t('auth.userManagement.email')} + + + + + + + + )} + + + + + + {t('auth.userManagement.displayName')} + + + + + + + + + 0 && !passwordValidation.isValid} isRequired={!isEdit}> + + + + {isEdit ? t('auth.userManagement.newPassword') : t('auth.userManagement.password')} + + + + + + + + : } + variant="ghost" + size="sm" + onClick={toggleShowPassword} + tabIndex={-1} + /> + + + + {password.length > 0 && !passwordValidation.isValid && ( + {passwordValidation.message} + )} + {password.length > 0 && passwordValidation.isValid && passwordValidation.message && ( + + {passwordValidation.message} + + )} + + + + + + + + + + + + + {t('auth.userManagement.isAdmin')} + + + + {error && ( + + {error} + + )} + + + + + + + +
+
+ ); +}); +UserFormModal.displayName = 'UserFormModal'; + +// --------------------------------------------------------------------------- +// Delete confirmation modal +// --------------------------------------------------------------------------- + +type DeleteUserModalProps = { + isOpen: boolean; + onClose: () => void; + user: UserDTO | null; +}; + +const DeleteUserModal = memo(({ isOpen, onClose, user }: DeleteUserModalProps) => { + const { t } = useTranslation(); + const [deleteUser, { isLoading }] = useDeleteUserMutation(); + const [error, setError] = useState(null); + + const handleDelete = useCallback(async () => { + if (!user) { + return; + } + setError(null); + try { + await deleteUser(user.user_id).unwrap(); + onClose(); + } catch (err) { + const detail = + err && typeof err === 'object' && 'data' in err && typeof (err as { data: unknown }).data === 'object' + ? ((err as { data: { detail?: string } }).data?.detail ?? t('auth.userManagement.deleteFailed')) + : t('auth.userManagement.deleteFailed'); + setError(detail); + } + }, [user, deleteUser, onClose, t]); + + const handleClose = useCallback(() => { + setError(null); + onClose(); + }, [onClose]); + + return ( + + + + {t('auth.userManagement.deleteUser')} + + + + {t('auth.userManagement.deleteConfirm', { + name: user?.display_name ?? user?.email ?? '', + })} + + {error && ( + + {error} + + )} + + + + + + + + ); +}); +DeleteUserModal.displayName = 'DeleteUserModal'; + +// --------------------------------------------------------------------------- +// Inline active/inactive toggle +// Wrapping the Switch in a Box lets the Tooltip track mouse-enter/leave +// correctly; without it the tooltip may not dismiss on mouse-out. +// --------------------------------------------------------------------------- + +const UserStatusToggle = memo(({ user, isCurrentUser }: { user: UserDTO; isCurrentUser: boolean }) => { + const { t } = useTranslation(); + const [updateUser, { isLoading }] = useUpdateUserMutation(); + + const handleChange = useCallback( + async (e: ChangeEvent) => { + await updateUser({ userId: user.user_id, data: { is_active: e.target.checked } }) + .unwrap() + .catch(() => null); + }, + [user.user_id, updateUser] + ); + + const tooltipLabel = isCurrentUser + ? t('auth.userManagement.cannotDeactivateSelf') + : user.is_active + ? t('auth.userManagement.deactivate') + : t('auth.userManagement.activate'); + + return ( + + + + + + ); +}); +UserStatusToggle.displayName = 'UserStatusToggle'; + +// --------------------------------------------------------------------------- +// Main component +// --------------------------------------------------------------------------- + +export const UserManagement = memo(() => { + const { t } = useTranslation(); + const currentUser = useAppSelector(selectCurrentUser); + const navigate = useNavigate(); + const { data: users, isLoading, error } = useListUsersQuery(); + + const createModal = useDisclosure(); + const editModal = useDisclosure(); + const deleteModal = useDisclosure(); + + const [selectedUser, setSelectedUser] = useState(null); + + const handleBack = useCallback(() => { + navigate(-1); + }, [navigate]); + + const handleEdit = useCallback( + (user: UserDTO) => { + setSelectedUser(user); + editModal.onOpen(); + }, + [editModal] + ); + + const handleDelete = useCallback( + (user: UserDTO) => { + setSelectedUser(user); + deleteModal.onOpen(); + }, + [deleteModal] + ); + + const handleEditClose = useCallback(() => { + editModal.onClose(); + setSelectedUser(null); + }, [editModal]); + + const handleDeleteClose = useCallback(() => { + deleteModal.onClose(); + setSelectedUser(null); + }, [deleteModal]); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ {t('auth.userManagement.loadFailed')} +
+ ); + } + + return ( + + + + + {t('auth.userManagement.title')} + + + + + + + + + + + + + + + + + {(users ?? []).map((user) => ( + + ))} + +
{t('auth.userManagement.email')}{t('auth.userManagement.displayName')}{t('auth.userManagement.role')}{t('auth.userManagement.status')}{t('auth.userManagement.actions')}
+
+ + {/* Create user modal */} + + + {/* Edit user modal */} + + + {/* Delete confirmation modal */} + +
+ ); +}); +UserManagement.displayName = 'UserManagement'; + +// --------------------------------------------------------------------------- +// User table row +// --------------------------------------------------------------------------- + +type UserRowProps = { + user: UserDTO; + isCurrentUser: boolean; + onEdit: (user: UserDTO) => void; + onDelete: (user: UserDTO) => void; +}; + +const UserRow = memo(({ user, isCurrentUser, onEdit, onDelete }: UserRowProps) => { + const { t } = useTranslation(); + + const handleEdit = useCallback(() => { + onEdit(user); + }, [user, onEdit]); + + const handleDelete = useCallback(() => { + onDelete(user); + }, [user, onDelete]); + + return ( + + + {user.email} + {isCurrentUser && ( + + {t('auth.userManagement.you')} + + )} + + + {user.display_name ?? '—'} + + + {user.is_admin ? ( + {t('auth.admin')} + ) : ( + {t('auth.userManagement.user')} + )} + + + + + + + + } + variant="ghost" + size="sm" + onClick={handleEdit} + /> + + + } + variant="ghost" + size="sm" + colorScheme="error" + isDisabled={isCurrentUser} + onClick={handleDelete} + /> + + + + + ); +}); +UserRow.displayName = 'UserRow'; diff --git a/invokeai/frontend/web/src/features/auth/components/UserMenu.tsx b/invokeai/frontend/web/src/features/auth/components/UserMenu.tsx new file mode 100644 index 00000000000..d8f598f996b --- /dev/null +++ b/invokeai/frontend/web/src/features/auth/components/UserMenu.tsx @@ -0,0 +1,87 @@ +import { Badge, Flex, IconButton, Menu, MenuButton, MenuItem, MenuList, Text, Tooltip } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { logout, selectCurrentUser } from 'features/auth/store/authSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiGearBold, PiSignOutBold, PiUserBold, PiUsersBold } from 'react-icons/pi'; +import { useNavigate } from 'react-router-dom'; +import { useLogoutMutation } from 'services/api/endpoints/auth'; + +export const UserMenu = memo(() => { + const { t } = useTranslation(); + const user = useAppSelector(selectCurrentUser); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const [logoutMutation] = useLogoutMutation(); + + const handleLogout = useCallback(() => { + // Call backend logout endpoint + logoutMutation() + .unwrap() + .catch(() => { + // Ignore errors - we'll log out locally anyway + }) + .finally(() => { + // Clear local state regardless of backend response + dispatch(logout()); + navigate('/login'); + }); + }, [dispatch, navigate, logoutMutation]); + + const handleProfile = useCallback(() => { + navigate('/profile'); + }, [navigate]); + + const handleUserManagement = useCallback(() => { + navigate('/admin/users'); + }, [navigate]); + + if (!user) { + return null; + } + + return ( + + + } + variant="link" + minW={8} + w={8} + h={8} + borderRadius="base" + /> + + + + + {user.display_name || user.email} + + + {user.email} + + {user.is_admin && ( + + {t('auth.admin')} + + )} + + } onClick={handleProfile}> + {t('auth.profile.menuItem')} + + {user.is_admin && ( + } onClick={handleUserManagement}> + {t('auth.userManagement.menuItem')} + + )} + } onClick={handleLogout}> + {t('auth.logout')} + + + + ); +}); + +UserMenu.displayName = 'UserMenu'; diff --git a/invokeai/frontend/web/src/features/auth/components/UserProfile.tsx b/invokeai/frontend/web/src/features/auth/components/UserProfile.tsx new file mode 100644 index 00000000000..02d25b6de98 --- /dev/null +++ b/invokeai/frontend/web/src/features/auth/components/UserProfile.tsx @@ -0,0 +1,393 @@ +import { + Box, + Button, + Center, + Flex, + FormControl, + FormErrorMessage, + FormHelperText, + FormLabel, + Grid, + GridItem, + Heading, + IconButton, + Input, + InputGroup, + InputRightElement, + Spinner, + Text, + Tooltip, + VStack, +} from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { selectAuthToken, selectCurrentUser, setCredentials } from 'features/auth/store/authSlice'; +import { validatePasswordField } from 'features/auth/util/passwordUtils'; +import type { ChangeEvent, FormEvent } from 'react'; +import { memo, useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiEyeBold, PiEyeSlashBold, PiLightningFill } from 'react-icons/pi'; +import { useNavigate } from 'react-router-dom'; +import { + useGetSetupStatusQuery, + useLazyGeneratePasswordQuery, + useUpdateCurrentUserMutation, +} from 'services/api/endpoints/auth'; + +const PASSWORD_GRID_COLUMNS = '180px 1fr'; + +export const UserProfile = memo(() => { + const { t } = useTranslation(); + const currentUser = useAppSelector(selectCurrentUser); + const currentToken = useAppSelector(selectAuthToken); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + const [displayName, setDisplayName] = useState(currentUser?.display_name ?? ''); + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showCurrentPassword, setShowCurrentPassword] = useState(false); + const [showNewPassword, setShowNewPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + + const [updateCurrentUser, { isLoading }] = useUpdateCurrentUserMutation(); + const [triggerGeneratePassword] = useLazyGeneratePasswordQuery(); + const { data: setupStatus } = useGetSetupStatusQuery(); + + const strictPasswordChecking = setupStatus?.strict_password_checking ?? true; + const newPasswordValidation = validatePasswordField(newPassword, t, strictPasswordChecking, true); + + const isPasswordChangeAttempted = newPassword.length > 0 || currentPassword.length > 0; + const passwordsMatch = newPassword.length > 0 && newPassword === confirmPassword; + const isPasswordChangeValid = + !isPasswordChangeAttempted || (currentPassword.length > 0 && newPasswordValidation.isValid && passwordsMatch); + + const handleCancel = useCallback(() => { + navigate(-1); + }, [navigate]); + + const handleGeneratePassword = useCallback(async () => { + try { + const result = await triggerGeneratePassword().unwrap(); + setNewPassword(result.password); + setConfirmPassword(result.password); + setShowNewPassword(true); + setShowConfirmPassword(true); + } catch { + // ignore + } + }, [triggerGeneratePassword]); + + const toggleShowCurrentPassword = useCallback(() => { + setShowCurrentPassword((v) => !v); + }, []); + + const toggleShowNewPassword = useCallback(() => { + setShowNewPassword((v) => !v); + }, []); + + const toggleShowConfirmPassword = useCallback(() => { + setShowConfirmPassword((v) => !v); + }, []); + + const handleDisplayNameChange = useCallback((e: ChangeEvent) => { + setDisplayName(e.target.value); + }, []); + + const handleCurrentPasswordChange = useCallback((e: ChangeEvent) => { + setCurrentPassword(e.target.value); + }, []); + + const handleNewPasswordChange = useCallback((e: ChangeEvent) => { + setNewPassword(e.target.value); + }, []); + + const handleConfirmPasswordChange = useCallback((e: ChangeEvent) => { + setConfirmPassword(e.target.value); + }, []); + + const handleSubmit = useCallback( + async (e: FormEvent) => { + e.preventDefault(); + setErrorMessage(null); + + if (!isPasswordChangeValid) { + return; + } + + try { + const updatePayload: Parameters[0] = { + display_name: displayName || null, + }; + if (newPassword) { + updatePayload.current_password = currentPassword; + updatePayload.new_password = newPassword; + } + const updatedUser = await updateCurrentUser(updatePayload).unwrap(); + + // Refresh the stored user info so the header reflects the new display name + if (currentToken) { + dispatch( + setCredentials({ + token: currentToken, + user: { + user_id: updatedUser.user_id, + email: updatedUser.email, + display_name: updatedUser.display_name ?? null, + is_admin: updatedUser.is_admin ?? false, + is_active: updatedUser.is_active ?? true, + }, + }) + ); + } + + // Navigate back after successful save + navigate(-1); + } catch (err) { + const detail = + err && typeof err === 'object' && 'data' in err && typeof (err as { data: unknown }).data === 'object' + ? ((err as { data: { detail?: string } }).data?.detail ?? t('auth.profile.saveFailed')) + : t('auth.profile.saveFailed'); + setErrorMessage(detail); + } + }, + [ + displayName, + currentPassword, + newPassword, + isPasswordChangeValid, + updateCurrentUser, + currentToken, + dispatch, + navigate, + t, + ] + ); + + if (!currentUser) { + return ( +
+ +
+ ); + } + + return ( + + + {t('auth.profile.title')} + + +
+ + {/* Email (read-only) */} + + {t('auth.profile.email')} + + {t('auth.profile.emailReadOnly')} + + + {/* Display name */} + + {t('auth.profile.displayName')} + + + + + + {t('auth.profile.changePassword')} + + + {/* Current password */} + 0}> + + + + {t('auth.profile.currentPassword')} + + + + + + + + : } + variant="ghost" + size="sm" + onClick={toggleShowCurrentPassword} + tabIndex={-1} + /> + + + + + + + + {/* New password */} + 0 && !newPasswordValidation.isValid} mb={4}> + + + + {t('auth.profile.newPassword')} + + + + + + + + : } + variant="ghost" + size="sm" + onClick={toggleShowNewPassword} + tabIndex={-1} + /> + + + + {newPassword.length > 0 && !newPasswordValidation.isValid && ( + {newPasswordValidation.message} + )} + {newPassword.length > 0 && newPasswordValidation.isValid && newPasswordValidation.message && ( + + {newPasswordValidation.message} + + )} + + + + + {/* Confirm new password */} + 0 && !passwordsMatch} mb={4}> + + + + {t('auth.profile.confirmPassword')} + + + + + + + + : } + variant="ghost" + size="sm" + onClick={toggleShowConfirmPassword} + tabIndex={-1} + /> + + + + {confirmPassword.length > 0 && !passwordsMatch && ( + {t('auth.profile.passwordsDoNotMatch')} + )} + + + + + {/* Generate password button – aligned with the input column */} + + + + + + + + + {errorMessage && ( + + {errorMessage} + + )} + + + + + + +
+
+ ); +}); +UserProfile.displayName = 'UserProfile'; diff --git a/invokeai/frontend/web/src/features/auth/store/authSlice.ts b/invokeai/frontend/web/src/features/auth/store/authSlice.ts new file mode 100644 index 00000000000..d933c57ed34 --- /dev/null +++ b/invokeai/frontend/web/src/features/auth/store/authSlice.ts @@ -0,0 +1,97 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import type { SliceConfig } from 'app/store/types'; +import { z } from 'zod'; + +const zUser = z.object({ + user_id: z.string(), + email: z.string(), + display_name: z.string().nullable(), + is_admin: z.boolean(), + is_active: z.boolean(), +}); + +const zAuthState = z.object({ + isAuthenticated: z.boolean(), + token: z.string().nullable(), + user: zUser.nullable(), + isLoading: z.boolean(), + sessionExpired: z.boolean(), +}); + +type User = z.infer; +type AuthState = z.infer; + +// Helper to safely access localStorage (not available in test environment) +const getStoredAuthToken = (): string | null => { + if (typeof window !== 'undefined' && window.localStorage) { + return localStorage.getItem('auth_token'); + } + return null; +}; + +const initialState: AuthState = { + isAuthenticated: !!getStoredAuthToken(), + token: getStoredAuthToken(), + user: null, + isLoading: false, + sessionExpired: false, +}; + +const getInitialAuthState = (): AuthState => initialState; + +const authSlice = createSlice({ + name: 'auth', + initialState, + reducers: { + setCredentials: (state, action: PayloadAction<{ token: string; user: User }>) => { + state.token = action.payload.token; + state.user = action.payload.user; + state.isAuthenticated = true; + state.sessionExpired = false; + if (typeof window !== 'undefined' && window.localStorage) { + localStorage.setItem('auth_token', action.payload.token); + } + }, + logout: (state) => { + state.token = null; + state.user = null; + state.isAuthenticated = false; + state.sessionExpired = false; + if (typeof window !== 'undefined' && window.localStorage) { + localStorage.removeItem('auth_token'); + } + }, + sessionExpiredLogout: (state) => { + state.token = null; + state.user = null; + state.isAuthenticated = false; + state.sessionExpired = true; + if (typeof window !== 'undefined' && window.localStorage) { + localStorage.removeItem('auth_token'); + } + }, + setLoading: (state, action: PayloadAction) => { + state.isLoading = action.payload; + }, + }, +}); + +export const { setCredentials, logout, sessionExpiredLogout, setLoading } = authSlice.actions; + +export const authSliceConfig: SliceConfig = { + slice: authSlice, + schema: zAuthState, + getInitialState: getInitialAuthState, + persistConfig: { + migrate: () => getInitialAuthState(), + // Don't persist auth state - token is stored in localStorage + persistDenylist: ['isAuthenticated', 'token', 'user', 'isLoading', 'sessionExpired'], + }, +}; + +export const selectIsAuthenticated = (state: { auth: AuthState }) => state.auth.isAuthenticated; +export const selectCurrentUser = (state: { auth: AuthState }) => state.auth.user; +export const selectAuthToken = (state: { auth: AuthState }) => state.auth.token; +export const selectIsAuthLoading = (state: { auth: AuthState }) => state.auth.isLoading; +export const selectSessionExpired = (state: { auth: AuthState }) => state.auth.sessionExpired; diff --git a/invokeai/frontend/web/src/features/auth/util/passwordUtils.ts b/invokeai/frontend/web/src/features/auth/util/passwordUtils.ts new file mode 100644 index 00000000000..53200d2c65f --- /dev/null +++ b/invokeai/frontend/web/src/features/auth/util/passwordUtils.ts @@ -0,0 +1,70 @@ +export type PasswordStrength = 'weak' | 'moderate' | 'strong'; + +export type PasswordValidationResult = { + isValid: boolean; + message: string; + strength: PasswordStrength | null; +}; + +/** + * Returns the strength level of a password. + * - weak: less than 8 characters + * - moderate: 8+ characters but missing uppercase, lowercase, or digit + * - strong: 8+ characters with uppercase, lowercase, and digit + */ +export const getPasswordStrength = (password: string): PasswordStrength => { + if (password.length < 8) { + return 'weak'; + } + const hasUpper = /[A-Z]/.test(password); + const hasLower = /[a-z]/.test(password); + const hasDigit = /\d/.test(password); + if (!hasUpper || !hasLower || !hasDigit) { + return 'moderate'; + } + return 'strong'; +}; + +/** + * Validates a password field. + * + * In strict mode, passwords must be 8+ characters with uppercase, lowercase, and digits. + * In non-strict mode, any non-empty password is accepted but strength is reported. + * + * @param password - The password to validate + * @param t - Translation function + * @param strictPasswordChecking - Whether to enforce strict requirements + * @param allowEmpty - When true, an empty string is treated as "no change" (valid with no message) + */ +export const validatePasswordField = ( + password: string, + t: (key: string) => string, + strictPasswordChecking: boolean, + allowEmpty = false +): PasswordValidationResult => { + if (password.length === 0) { + return { isValid: allowEmpty, message: '', strength: null }; + } + + const strength = getPasswordStrength(password); + + if (!strictPasswordChecking) { + return { + isValid: true, + message: t(`auth.passwordStrength.${strength}`), + strength, + }; + } + + // Strict mode + if (password.length < 8) { + return { isValid: false, message: t('auth.setup.passwordTooShort'), strength }; + } + const hasUpper = /[A-Z]/.test(password); + const hasLower = /[a-z]/.test(password); + const hasDigit = /\d/.test(password); + if (!hasUpper || !hasLower || !hasDigit) { + return { isValid: false, message: t('auth.setup.passwordMissingRequirements'), strength }; + } + return { isValid: true, message: '', strength }; +}; diff --git a/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx b/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx new file mode 100644 index 00000000000..5ac6ffcb7c9 --- /dev/null +++ b/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx @@ -0,0 +1,126 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox, ConfirmationAlertDialog, Flex, FormControl, Text } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { selectCurrentUser } from 'features/auth/store/authSlice'; +import { + changeBoardReset, + isModalOpenChanged, + selectChangeBoardModalSlice, +} from 'features/changeBoardModal/store/slice'; +import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors'; +import { memo, useCallback, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; +import { useAddImagesToBoardMutation, useRemoveImagesFromBoardMutation } from 'services/api/endpoints/images'; +import type { BoardDTO } from 'services/api/types'; + +const selectImagesToChange = createSelector( + selectChangeBoardModalSlice, + (changeBoardModal) => changeBoardModal.image_names +); + +const selectIsModalOpen = createSelector( + selectChangeBoardModalSlice, + (changeBoardModal) => changeBoardModal.isModalOpen +); + +const ChangeBoardModal = () => { + useAssertSingleton('ChangeBoardModal'); + const dispatch = useAppDispatch(); + const currentBoardId = useAppSelector(selectSelectedBoardId); + const currentUser = useAppSelector(selectCurrentUser); + const [selectedBoardId, setSelectedBoardId] = useState(); + const { data: boards, isFetching } = useListAllBoardsQuery({ include_archived: true }); + const isModalOpen = useAppSelector(selectIsModalOpen); + const imagesToChange = useAppSelector(selectImagesToChange); + const [addImagesToBoard] = useAddImagesToBoardMutation(); + const [removeImagesFromBoard] = useRemoveImagesFromBoardMutation(); + const { t } = useTranslation(); + + // Returns true if the current user can write images to the given board. + const canWriteToBoard = useCallback( + (board: BoardDTO): boolean => { + const isOwnerOrAdmin = !currentUser || currentUser.is_admin || board.user_id === currentUser.user_id; + return isOwnerOrAdmin || board.board_visibility === 'public'; + }, + [currentUser] + ); + + const options = useMemo(() => { + return [{ label: t('boards.uncategorized'), value: 'none' }] + .concat( + (boards ?? []) + .filter(canWriteToBoard) + .map((board) => ({ + label: board.board_name, + value: board.board_id, + })) + .sort((a, b) => a.label.localeCompare(b.label)) + ) + .filter((board) => board.value !== currentBoardId); + }, [boards, canWriteToBoard, currentBoardId, t]); + + const value = useMemo(() => options.find((o) => o.value === selectedBoardId), [options, selectedBoardId]); + + const handleClose = useCallback(() => { + dispatch(changeBoardReset()); + dispatch(isModalOpenChanged(false)); + }, [dispatch]); + + const handleChangeBoard = useCallback(() => { + if (!selectedBoardId || imagesToChange.length === 0) { + return; + } + + if (imagesToChange.length) { + if (selectedBoardId === 'none') { + removeImagesFromBoard({ image_names: imagesToChange }); + } else { + addImagesToBoard({ + image_names: imagesToChange, + board_id: selectedBoardId, + }); + } + } + dispatch(changeBoardReset()); + }, [addImagesToBoard, dispatch, imagesToChange, removeImagesFromBoard, selectedBoardId]); + + const onChange = useCallback((v) => { + if (!v) { + return; + } + setSelectedBoardId(v.value); + }, []); + + return ( + + + + {t('boards.movingImagesToBoard', { + count: imagesToChange.length, + })} + + + + + + + ); +}; + +export default memo(ChangeBoardModal); diff --git a/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts b/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts new file mode 100644 index 00000000000..3f72720a420 --- /dev/null +++ b/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts @@ -0,0 +1,40 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import type { RootState } from 'app/store/store'; +import type { SliceConfig } from 'app/store/types'; +import z from 'zod'; + +const zChangeBoardModalState = z.object({ + isModalOpen: z.boolean().default(false), + image_names: z.array(z.string()).default(() => []), +}); +type ChangeBoardModalState = z.infer; + +const getInitialState = (): ChangeBoardModalState => zChangeBoardModalState.parse({}); + +const slice = createSlice({ + name: 'changeBoardModal', + initialState: getInitialState(), + reducers: { + isModalOpenChanged: (state, action: PayloadAction) => { + state.isModalOpen = action.payload; + }, + imagesToChangeSelected: (state, action: PayloadAction) => { + state.image_names = action.payload; + }, + changeBoardReset: (state) => { + state.image_names = []; + state.isModalOpen = false; + }, + }, +}); + +export const { isModalOpenChanged, imagesToChangeSelected, changeBoardReset } = slice.actions; + +export const selectChangeBoardModalSlice = (state: RootState) => state.changeBoardModal; + +export const changeBoardModalSliceConfig: SliceConfig = { + slice, + schema: zChangeBoardModalState, + getInitialState, +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/README.md b/invokeai/frontend/web/src/features/controlLayers/README.md new file mode 100644 index 00000000000..de2aafee13a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/README.md @@ -0,0 +1,228 @@ +# Canvas + +The canvas is a fairly complex feature. It uses "native" KonvaJS (i.e. not the Konva react bindings) to render a drawing canvas. + +It supports layers, drawing, erasing, undo/redo, exporting, backend filters (i.e. filters that require sending image data to teh backend to process) and frontend filters. + +## Broad Strokes of Design + +The canvas is internally is a hierarchy of classes (modules). All canvas modules inherit from invokeai/frontend/web/src/features/controlLayers/konva/CanvasModuleBase.ts + +### Modules + +The top-level module is the CanvasManager: invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts + +All canvas modules have: + +- A unique id (per instance) +- A ref to its parent module and the canvas manager (the top-leve Manager refs itself) +- A repr() method that returns a plain JS object representing the module instance +- A destroy() method to clean up resources +- A log() method that auto-injects context for the module instanc) + +Modules can do anything, they are simply plain-JS classes to encapsulate some functionality. Some are singletons. Some examples: + +- A singleton module that handles tool-specific interactions: invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasToolModule.ts +- Singleton models for each tool e.g. the CanvasBrushToolModule: invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasBrushToolModule.ts +- A singleton module to render the background of the canvas: invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackgroundModule.ts +- A strictly logical module that manages various caches of image data: invokeai/frontend/web/src/features/controlLayers/konva/CanvasCacheModule.ts +- A non-singleton module that handles rendering a brush stroke: invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject/CanvasObjectBrushLine.ts + +### Layers (Entities) and Adapters modules + +Canvas has a number of layer types: + +- Raster layers: Traditional raster/pixel layers, much like layers in Photoshop +- Control layers: Internally a raster layer, but designated to hold control data (e.g. depth maps, segmentation masks, etc.) and have special rendering rules +- Regional guidance layers: A mask-like layer (i.e. it has arbitrary shapes but they have no color or texture, it's just a mask region) plus conditioning data like prompts or ref images. The conditioning is applied only to the masked regions +- Inpaint mask layers: Another mask-like layer that indicate regions to inpaint/regenerate + +Instances of layers are called "entities" in the codebase. Each entity has a type (one of the above), a number of properties (e.g. visibility, opacity, etc.), objects (e.g. brush strokes, shapes, images) and possibly other data. + +Each layer type has a corresponding "adapter" module that handles rendering the layer and its objects, applying filters, etc. The adapter modules are non-singleton modules that are instantiated once per layer entity. + +Using the raster layer type as an example, it has a number of sub-modules: + +- A top-level module that coordinates everything: invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer.ts +- An object (e.g. brush strokes, shapes, images) renderer that draws the layer via Konva: invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer.ts +- A "buffer" object renderer, which renders in-progress objects (e.g. a brush stroke that is being drawn but not yet committed, important for performance): invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityBufferObjectRenderer.ts +- A module that handles previewing and applying backend filters: invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityFilterer.ts +- A module that handles selecting objects from the pixel data of a layer (aka segmentation tasks): invokeai/frontend/web/src/features/controlLayers/konva/CanvasSegmentAnythingModule.ts +- A module that handles transforming the layer (scale, translate, rotate): invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityTransformer.ts + +## State mgmt + +This gets a bit hairy. We have a mix of redux, Konva and nanostores. + +At a high level, we use observable/listener patterns to react to state changes and propagate them to where they need to go. + +### Redux + +Redux is the source of truth for _persistent_ canvas state - layers, their order, etc. + +The redux API includes: + +- getState(): Get the entire redux state +- subscribe(listener): Subscribe to state changes, listener is called on _every_ state change, no granularity is provided +- dispatch(action): Dispatch an action to change state + +Redux is not suitable for _transient_ state that changes frequently, e.g. the current brush stroke as the user is drawing it. Syncing every change to redux would be too slow and incur a significant performance penalty that would drop FPS too much. + +Canvas modules that have persistent state (e.g. layers, their properties, etc.) use redux to store that state and will subscribe to redux to listen for changes and update themselves as needed. + +### Konva + +Konva's API is imperative (i.e. you call methods on the Konva nodes to change them) but it renders automatically. + +There is no simple way to "subscribe" to changes in Konva nodes. You can listen to certain events (e.g. dragmove, transform, etc.) but there is no generic "node changed" event. + +So we almost exclusively push data to Konva, we never "read" from it. + +### Nanostores + +We use https://github.com/nanostores/nanostores as a lightweight observable state management solution. Nanostores has a plain-JS listener API for subscribing to changes, similar to redux's subscribe(). And it has react bindings so we can use it in react components. + +Modules often use nanostores to store their internal state, especially when that state needs to be observed by other modules or react components. + +For example, the CanvasToolModule uses a nanostore to hold the current tool (brush, eraser, etc.) and its options (brush size, color, etc.). React components can subscribe to that store to update their UI when the tool or its options change. + +So this provides a simple two-way binding between canvas modules and react components. + +### State -> Canvas + +Data may flow from redux state to Canvas. For example, on canvas init we render all layers and their objects from redux state in Konva: + +- Create the layer's entity adapter and all sub-modules +- Iterate over the layer's objects and create a module instance for each object (e.g. brush stroke, shape, image) +- Each object module creates the necessary Konva nodes to represent itself and adds them to the layer + +The entity adapter subscribes to redux to listen for state changes and pass on the updated state to its sub-modules so they can do whatever they need to do w/ the updated state. + +Besides the initial render, we might have to update the Konva representation of a layer when: + +- The layer's properties are changed (e.g. visibility, opacity, etc.) +- The layer's order is changed (e.g. move up/down) +- User does an undo/redo operation that affects the layer +- The layer is deleted + +### Canvas -> State + +When the user interacts w/ the canvas (e.g. draws a brush stroke, erases, moves an object, etc.), we create/update/delete objects in Konva. When the user finishes the interaction (e.g. finishes drawing a brush stroke), we serialize the object to a plain JS object and dispatch a redux action to add the object in redux state. + +Using drawing a line on a raster layer as an example, the flow is: + +- User initiates a brush stroke and draws +- We create a brush line object module instance in the layer's buffer renderer +- The brush line object is given a unique ID +- The brush line mod creates a Konva.Line node to represent the stroke +- The brush line mod tracks the stroke as the user draws, updating the Konva.Line node as needed, all in the buffer renderer +- When the user finishes the stroke, the brush line module transfers control of itself from the layer's buffer renderer to its main renderer +- As the line is marked complete, the line data is serialized to a plain JS object (i.e. array of points and color) and we dispatch a redux action to add the line object to the layer entity in redux state + +Besides drawing tasks, we have similar flows for: + +- Transforming a layer (scale, translate, rotate) +- Filtering a layer +- Selecting objects from a layer (segmentation tasks) + +## Erasing is hard + +HTML Canvas has a limited set of compositing modes. These apply globally to the whole canvas element. There is no "local" compositing mode that applies only to a specific shape or object. There is no concept of layers. + +So to implement erasing (and opacity!), we have to get creative. Konva handles much of this for us. Each layer is represented internally by a Konva.Layer, which in turn is drawn to its own HTML Canvas element. + +Erasing is accomplished by using a globalCompositeOperation of "destination-out" on the brush stroke that is doing the erasing. The brush stroke "cuts a hole" in the layer it is drawn on. + +There is a complication. The UX for erasing a layer should be: + +- User has a layer, let's say it has an image on it +- The layer's size is exactly the size of the image +- User erases the right-hand half of the image +- The layer's size shrinks to fit the remaining content, i.e. the left half of the image +- If the user transforms the layer (scale, translate, rotate), the transformations apply only to the remaining content + +But the "destination-out" compositing mode only makes the erased pixels transparent. It does not actually remove them from the layer. The layer's bounding box includes the eraser strokes - even though they are transparent. The eraser strokes can actually _enlarge_ the layer's bounding box if the user erases outside the original bounds of the layer. + +So, we need a way to calculate the _visual_ bounds of the layer, i.e. the bounding box of all non-transparent pixels. We do this by rendering the layer to an offscreen canvas and reading back the pixel data to calculate the bounds. This process is costly, and we offload some of the work to a web worker to avoid blocking the main thread. Nevertheless, just getting that pixel data is expensive, scaling to the size of the layer. + +The usage of the buffer renderer module helps a lot here, as we only need to recalc the bounds when the user finishes a drawing action, not while they are drawing it. + +You'll see the relevant code for this in the transformer module. It encapsulates the bounds calculation logic and exposes an observable that holds the last-known visual bounds of the layer. + +The worker entrypoint is here invokeai/frontend/web/src/features/controlLayers/konva/CanvasWorkerModule.ts + +## Rasterizing layers + +Layers consist of a mix of vector and pixel data. For example, a brush stroke is a vector (i.e. array of points) and an image is pixel data. + +Ideally we could go straight from user input to pixel data, but this is not feasible for performance reasons. We'd need to write the images to an offscreen canvas, read back the pixel data, send it to the backend, get back the processed pixel data, write it to an offscreen canvas, then read back the pixel data again to update the layer. This would be too slow and block the main thread too much. + +So we use a hybrid approach. We keep the vector data in memory and render it to pixel data only when needed, e.g. when the user applies a backend filter or does a transformation on the canvas. + +This is unfortunately complicated but we couldn't figure out a more performance way to handle this. + +## Compositing layers to prepare for generation + +The canvas is a means to an end: provide strong user control and agency for image generation. + +When generating an image, the raster layers must be composited toegher into a single image that is sent to the backend. All inpaint masks are similarly composited together into a single mask image. Regional guidance and control layers are not composited together, they are sent as individual images. + +This is handled in invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts + +For each compositing task, the compositor creates a unique hash of the layer's state (e.g. objects, properties, etc.) and uses that to cache the resulting composited image's name (which ref a unique ref to the image file stored on disk). This avoids re-compositing layers that haven't changed since the last generation. + +## The generation bounding box + +Image generation models can only generate images up to certain sizes without causing VRAM OOMs. So we need to give the user a way to specify the size of the generation area. This is done via the "generation bounding box" tool, which is a rectangle that the user can resize and move around the canvas. + +Here's the module for it invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasBboxToolModule.ts + +Models all have width/height constraints - they must be multiples of a certain number (typically 8, 16 or 32). This is related to the internal "latents" representatino of images in diffusion models. So the generation bbox must be constrained to these multiples. + +## Staging generations + +The typical use pattern for generating images on canvas is to generate a number of variations and pick one or more to keep. This is supported via the "staging area", which is a horizontal strip of image thumbnails below the canvas. These staged images are rendered via React, not Konva. + +Once canvas generation starts, much of the canvas is locked down until the user finalizes the staging area, either by accepting a single image, adding one or more images as new layers, or discarding all staged images. + +The currently-selected staged image is previewed on the canvas and rendered via invokeai/frontend/web/src/features/controlLayers/konva/CanvasStagingAreaModule.ts + +When the user accepts a staged image, it is added as a new raster layer (there are other options for adding as control, saving directly to gallery, etc). + +This subsystem tracks generated images by watching the queue of generation tasks. The relevant code for queue tracking is in invokeai/frontend/web/src/features/controlLayers/components/StagingArea/state.ts + +## Future enhancements + +### Perf: Reduce the number of canvas elements + +Each layer has a Konva.Layer which has its own canvas element. Once you get too many of these, the browser starts to struggle. + +One idea to improve this would be to have a 3-layer system: + +- The active layer is its own Konva.Layer +- All layers behind it are flattened into a single Konva.Layer +- All layers in front of it are flattened into a single Konva.Layer + +When the user switches the active layer, we re-flatten the layers as needed. This would reduce the number of canvas elements to 3 regardless of how many layers there are. This would greatly improve performance, especially on lower-end devices. + +### Perf: Konva in a web worker + +All of the heavy konva rendering could be offloaded to a web worker. This would free up the main thread for user interactions and UI updates. The main thread would send user input and state changes to the worker, and the worker would send back rendered images to display. + +There used to be a hacky example of this on the Konva docs but I can't find it as of this writing. It requires proxying mouse and keyboard events to the worker, but wasn't too complicated. This could be a _huge_ perf win. + +### Abstract state bindings + +Currently the state bindings (redux, nanostores) are all over the place. There is a singleton module that handles much of the redux binding, but it's still a bit messy: invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts + +Many modules still directly subscribe to redux with their own selectors. + +Ideally we could have a more abstracted state binding system that could handle multiple backends (e.g. redux, nanostores, etc.) in a more uniform way. This would make it easier to manage state and reduce boilerplate code. + +### Do not lock down canvas as much during staging + +Currently, once the user starts generating images, much of the canvas is locked down until the user finalizes the staging area. This can be frustrating if the user wants to make small adjustments to layers or settings while previewing staged images, but it prevents footguns. + +For example, if the user changes the generation bbox size while staging, then queues up more generations, the output images may not match the bbox size, leading to confusion. + +It's more locked-down than it needs to be. Theoretically, most of the canvas could be interactive while staging. Just needs some careful through to not be too confusing. diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx new file mode 100644 index 00000000000..88b5b6d37ea --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAddEntityButtons.tsx @@ -0,0 +1,99 @@ +import { Button, Flex, Heading } from '@invoke-ai/ui-library'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { + useAddControlLayer, + useAddInpaintMask, + useAddNewRegionalGuidanceWithARefImage, + useAddRasterLayer, + useAddRegionalGuidance, +} from 'features/controlLayers/hooks/addLayerHooks'; +import { useIsEntityTypeEnabled } from 'features/controlLayers/hooks/useIsEntityTypeEnabled'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiPlusBold } from 'react-icons/pi'; + +export const CanvasAddEntityButtons = memo(() => { + const { t } = useTranslation(); + const addInpaintMask = useAddInpaintMask(); + const addRegionalGuidance = useAddRegionalGuidance(); + const addRasterLayer = useAddRasterLayer(); + const addControlLayer = useAddControlLayer(); + const addRegionalReferenceImage = useAddNewRegionalGuidanceWithARefImage(); + const isRegionalGuidanceEnabled = useIsEntityTypeEnabled('regional_guidance'); + const isControlLayerEnabled = useIsEntityTypeEnabled('control_layer'); + const isInpaintLayerEnabled = useIsEntityTypeEnabled('inpaint_mask'); + + return ( + + + + {t('controlLayers.regional')} + + + + + + + + + + + + {t('controlLayers.layer_other')} + + + + + + + + + + ); +}); + +CanvasAddEntityButtons.displayName = 'CanvasAddEntityButtons'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsBboxVisibility.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsBboxVisibility.tsx new file mode 100644 index 00000000000..32b51e888e2 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsBboxVisibility.tsx @@ -0,0 +1,24 @@ +import { Alert, AlertIcon, AlertTitle } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const CanvasAlertsBboxVisibility = memo(() => { + const { t } = useTranslation(); + const canvasManager = useCanvasManager(); + const isBboxHidden = useStore(canvasManager.tool.tools.bbox.$isBboxHidden); + + if (!isBboxHidden) { + return null; + } + + return ( + + + {t('controlLayers.warnings.bboxHidden')} + + ); +}); + +CanvasAlertsBboxVisibility.displayName = 'CanvasAlertsBboxVisibility'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress.tsx new file mode 100644 index 00000000000..eb2a043864b --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress.tsx @@ -0,0 +1,55 @@ +import { Alert, AlertDescription, AlertIcon, AlertTitle } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useDeferredModelLoadingInvocationProgressMessage } from 'features/controlLayers/hooks/useDeferredModelLoadingInvocationProgressMessage'; +import { selectSystemShouldShowInvocationProgressDetail } from 'features/system/store/systemSlice'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { $lastProgressMessage } from 'services/events/stores'; + +const CanvasAlertsInvocationProgressContentLocal = memo(() => { + const { t } = useTranslation(); + const invocationProgressMessage = useStore($lastProgressMessage); + + if (!invocationProgressMessage) { + return null; + } + + return ( + + + {t('common.generating')} + {invocationProgressMessage} + + ); +}); +CanvasAlertsInvocationProgressContentLocal.displayName = 'CanvasAlertsInvocationProgressContentLocal'; + +const CanvasAlertsInvocationProgressContentCommercial = memo(() => { + const message = useDeferredModelLoadingInvocationProgressMessage(); + + if (!message) { + return null; + } + + return ( + + + {message} + + ); +}); +CanvasAlertsInvocationProgressContentCommercial.displayName = 'CanvasAlertsInvocationProgressContentCommercial'; + +export const CanvasAlertsInvocationProgress = memo(() => { + const shouldShowInvocationProgressDetail = useAppSelector(selectSystemShouldShowInvocationProgressDetail); + + // user setting + if (!shouldShowInvocationProgressDetail) { + return null; + } + + return ; +}); + +CanvasAlertsInvocationProgress.displayName = 'CanvasAlertsInvocationProgress'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask.tsx new file mode 100644 index 00000000000..7178c3d123b --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask.tsx @@ -0,0 +1,23 @@ +import { Alert, AlertIcon, AlertTitle } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectPreserveMask } from 'features/controlLayers/store/canvasSettingsSlice'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const CanvasAlertsPreserveMask = memo(() => { + const { t } = useTranslation(); + const preserveMask = useAppSelector(selectPreserveMask); + + if (!preserveMask) { + return null; + } + + return ( + + + {t('controlLayers.settings.preserveMask.alert')} + + ); +}); + +CanvasAlertsPreserveMask.displayName = 'CanvasAlertsPreserveMask'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSaveAllImagesToGallery.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSaveAllImagesToGallery.tsx new file mode 100644 index 00000000000..5a4da84bfe1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSaveAllImagesToGallery.tsx @@ -0,0 +1,23 @@ +import { Alert, AlertIcon, AlertTitle } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectSaveAllImagesToGallery } from 'features/controlLayers/store/canvasSettingsSlice'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const CanvasAlertsSaveAllImagesToGallery = memo(() => { + const { t } = useTranslation(); + const saveAllImagesToGallery = useAppSelector(selectSaveAllImagesToGallery); + + if (!saveAllImagesToGallery) { + return null; + } + + return ( + + + {t('controlLayers.settings.saveAllImagesToGallery.alert')} + + ); +}); + +CanvasAlertsSaveAllImagesToGallery.displayName = 'CanvasAlertsSaveAllImagesToGallery'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus.tsx new file mode 100644 index 00000000000..c7ec2151a3b --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus.tsx @@ -0,0 +1,132 @@ +import type { AlertStatus } from '@invoke-ai/ui-library'; +import { Alert, AlertIcon, AlertTitle } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate'; +import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext'; +import { useEntityTitle } from 'features/controlLayers/hooks/useEntityTitle'; +import { useEntityTypeIsHidden } from 'features/controlLayers/hooks/useEntityTypeIsHidden'; +import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types'; +import { + selectCanvasSlice, + selectEntityOrThrow, + selectSelectedEntityIdentifier, +} from 'features/controlLayers/store/selectors'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { atom } from 'nanostores'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +type ContentProps = { + entityIdentifier: CanvasEntityIdentifier; + adapter: CanvasEntityAdapter; +}; + +const $isFilteringFallback = atom(false); + +type AlertData = { + status: AlertStatus; + title: string; +}; + +const buildSelectIsEnabled = (entityIdentifier: CanvasEntityIdentifier) => + createSelector( + selectCanvasSlice, + (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'CanvasAlertsSelectedEntityStatusContent').isEnabled + ); + +const buildSelectIsLocked = (entityIdentifier: CanvasEntityIdentifier) => + createSelector( + selectCanvasSlice, + (canvas) => selectEntityOrThrow(canvas, entityIdentifier, 'CanvasAlertsSelectedEntityStatusContent').isLocked + ); + +const CanvasAlertsSelectedEntityStatusContent = memo(({ entityIdentifier, adapter }: ContentProps) => { + const { t } = useTranslation(); + const title = useEntityTitle(entityIdentifier); + const selectIsEnabled = useMemo(() => buildSelectIsEnabled(entityIdentifier), [entityIdentifier]); + const selectIsLocked = useMemo(() => buildSelectIsLocked(entityIdentifier), [entityIdentifier]); + const isEnabled = useAppSelector(selectIsEnabled); + const isLocked = useAppSelector(selectIsLocked); + const isHidden = useEntityTypeIsHidden(entityIdentifier.type); + const isFiltering = useStore(adapter.filterer?.$isFiltering ?? $isFilteringFallback); + const isTransforming = useStore(adapter.transformer.$isTransforming); + const isEmpty = useStore(adapter.$isEmpty); + + const alert = useMemo(() => { + if (isFiltering) { + return { + status: 'warning', + title: t('controlLayers.HUD.entityStatus.isFiltering', { title }), + }; + } + + if (isTransforming) { + return { + status: 'warning', + title: t('controlLayers.HUD.entityStatus.isTransforming', { title }), + }; + } + + if (isEmpty) { + return { + status: 'warning', + title: t('controlLayers.HUD.entityStatus.isEmpty', { title }), + }; + } + + if (isHidden) { + return { + status: 'warning', + title: t('controlLayers.HUD.entityStatus.isHidden', { title }), + }; + } + + if (isLocked) { + return { + status: 'warning', + title: t('controlLayers.HUD.entityStatus.isLocked', { title }), + }; + } + + if (!isEnabled) { + return { + status: 'warning', + title: t('controlLayers.HUD.entityStatus.isDisabled', { title }), + }; + } + + return null; + }, [isFiltering, isTransforming, isEmpty, isHidden, isLocked, isEnabled, title, t]); + + if (!alert) { + return null; + } + + return ( + + + {alert.title} + + ); +}); + +CanvasAlertsSelectedEntityStatusContent.displayName = 'CanvasAlertsSelectedEntityStatusContent'; + +export const CanvasAlertsSelectedEntityStatus = memo(() => { + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const adapter = useEntityAdapterSafe(selectedEntityIdentifier); + + if (!selectedEntityIdentifier || !adapter) { + return null; + } + + return ( + + + + ); +}); + +CanvasAlertsSelectedEntityStatus.displayName = 'CanvasAlertsSelectedEntityStatus'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsTextSessionActive.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsTextSessionActive.tsx new file mode 100644 index 00000000000..c5570b1fc3a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsTextSessionActive.tsx @@ -0,0 +1,24 @@ +import { Alert, AlertIcon, AlertTitle } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const CanvasAlertsTextSessionActive = memo(() => { + const { t } = useTranslation(); + const canvasManager = useCanvasManager(); + const session = useStore(canvasManager.tool.tools.text.$session); + + if (!session || session.status === 'committed') { + return null; + } + + return ( + + + {t('controlLayers.HUD.textSessionActive')} + + ); +}); + +CanvasAlertsTextSessionActive.displayName = 'CanvasAlertsTextSessionActive'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAutoProcessSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAutoProcessSwitch.tsx new file mode 100644 index 00000000000..7137fb3b6de --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAutoProcessSwitch.tsx @@ -0,0 +1,24 @@ +import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { selectAutoProcess, settingsAutoProcessToggled } from 'features/controlLayers/store/canvasSettingsSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const CanvasAutoProcessSwitch = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const autoProcess = useAppSelector(selectAutoProcess); + + const onChange = useCallback(() => { + dispatch(settingsAutoProcessToggled()); + }, [dispatch]); + + return ( + + {t('controlLayers.filter.autoProcess')} + + + ); +}); + +CanvasAutoProcessSwitch.displayName = 'CanvasAutoProcessSwitch'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasBusySpinner.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasBusySpinner.tsx new file mode 100644 index 00000000000..e67aff300ab --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasBusySpinner.tsx @@ -0,0 +1,29 @@ +import type { SpinnerProps } from '@invoke-ai/ui-library'; +import { Spinner } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { useAllEntityAdapters } from 'features/controlLayers/contexts/EntityAdapterContext'; +import { computed } from 'nanostores'; +import { memo, useMemo } from 'react'; + +export const CanvasBusySpinner = memo((props: SpinnerProps) => { + const canvasManager = useCanvasManager(); + const allEntityAdapters = useAllEntityAdapters(); + const $isPendingRectCalculation = useMemo( + () => + computed( + allEntityAdapters.map(({ transformer }) => transformer.$isPendingRectCalculation), + (...values) => values.some((v) => v) + ), + [allEntityAdapters] + ); + const isPendingRectCalculation = useStore($isPendingRectCalculation); + const isRasterizing = useStore(canvasManager.stateApi.$isRasterizing); + const isCompositing = useStore(canvasManager.compositor.$isBusy); + + if (isRasterizing || isCompositing || isPendingRectCalculation) { + return ; + } + return null; +}); +CanvasBusySpinner.displayName = 'CanvasBusySpinner'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems.tsx new file mode 100644 index 00000000000..064378b2274 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems.tsx @@ -0,0 +1,114 @@ +import { Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library'; +import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu'; +import { CanvasContextMenuItemsCropCanvasToBbox } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuItemsCropCanvasToBbox'; +import { NewLayerIcon } from 'features/controlLayers/components/common/icons'; +import { useLoadCanvasProjectWithDialog } from 'features/controlLayers/components/LoadCanvasProjectConfirmationAlertDialog'; +import { useSaveCanvasProjectWithDialog } from 'features/controlLayers/components/SaveCanvasProjectDialog'; +import { useCopyCanvasToClipboard } from 'features/controlLayers/hooks/copyHooks'; +import { + useNewControlLayerFromBbox, + useNewGlobalReferenceImageFromBbox, + useNewRasterLayerFromBbox, + useNewRegionalReferenceImageFromBbox, + useSaveBboxToGallery, + useSaveCanvasToGallery, +} from 'features/controlLayers/hooks/saveCanvasHooks'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiArchiveBold, PiCopyBold, PiFileArrowDownBold, PiFileArrowUpBold, PiFloppyDiskBold } from 'react-icons/pi'; + +export const CanvasContextMenuGlobalMenuItems = memo(() => { + const { t } = useTranslation(); + const saveSubMenu = useSubMenu(); + const projectSubMenu = useSubMenu(); + const newSubMenu = useSubMenu(); + const copySubMenu = useSubMenu(); + const isBusy = useCanvasIsBusy(); + const saveCanvasToGallery = useSaveCanvasToGallery(); + const saveBboxToGallery = useSaveBboxToGallery(); + const saveCanvasProject = useSaveCanvasProjectWithDialog(); + const loadCanvasProject = useLoadCanvasProjectWithDialog(); + const newRegionalReferenceImageFromBbox = useNewRegionalReferenceImageFromBbox(); + const newGlobalReferenceImageFromBbox = useNewGlobalReferenceImageFromBbox(); + const newRasterLayerFromBbox = useNewRasterLayerFromBbox(); + const newControlLayerFromBbox = useNewControlLayerFromBbox(); + const copyCanvasToClipboard = useCopyCanvasToClipboard('canvas'); + const copyBboxToClipboard = useCopyCanvasToClipboard('bbox'); + + return ( + <> + + + }> + + + + + + } isDisabled={isBusy} onClick={saveCanvasToGallery}> + {t('controlLayers.canvasContextMenu.saveCanvasToGallery')} + + } isDisabled={isBusy} onClick={saveBboxToGallery}> + {t('controlLayers.canvasContextMenu.saveBboxToGallery')} + + + + + }> + + + + + + } isDisabled={isBusy} onClick={saveCanvasProject}> + {t('controlLayers.canvasProject.saveProject')} + + } isDisabled={isBusy} onClick={loadCanvasProject}> + {t('controlLayers.canvasProject.loadProject')} + + + + + }> + + + + + + } isDisabled={isBusy} onClick={newGlobalReferenceImageFromBbox}> + {t('controlLayers.canvasContextMenu.newGlobalReferenceImage')} + + } isDisabled={isBusy} onClick={newRegionalReferenceImageFromBbox}> + {t('controlLayers.canvasContextMenu.newRegionalReferenceImage')} + + } isDisabled={isBusy} onClick={newControlLayerFromBbox}> + {t('controlLayers.canvasContextMenu.newControlLayer')} + + } isDisabled={isBusy} onClick={newRasterLayerFromBbox}> + {t('controlLayers.canvasContextMenu.newRasterLayer')} + + + + + }> + + + + + + } isDisabled={isBusy} onClick={copyCanvasToClipboard}> + {t('controlLayers.canvasContextMenu.copyCanvasToClipboard')} + + } isDisabled={isBusy} onClick={copyBboxToClipboard}> + {t('controlLayers.canvasContextMenu.copyBboxToClipboard')} + + + + + + + ); +}); + +CanvasContextMenuGlobalMenuItems.displayName = 'CanvasContextMenuGlobalMenuItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuItemsCropCanvasToBbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuItemsCropCanvasToBbox.tsx new file mode 100644 index 00000000000..5f034ed7976 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuItemsCropCanvasToBbox.tsx @@ -0,0 +1,26 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCropBold } from 'react-icons/pi'; + +export const CanvasContextMenuItemsCropCanvasToBbox = memo(() => { + const { t } = useTranslation(); + const isBusy = useCanvasIsBusy(); + const canvasManager = useCanvasManager(); + const cropCanvasToBbox = useCallback(async () => { + const adapters = canvasManager.getAllAdapters(); + for (const adapter of adapters) { + await adapter.cropToBbox(); + } + }, [canvasManager]); + + return ( + } isDisabled={isBusy} onClick={cropCanvasToBbox}> + {t('controlLayers.canvasContextMenu.cropCanvasToBbox')} + + ); +}); + +CanvasContextMenuItemsCropCanvasToBbox.displayName = 'CanvasContextMenuItemsCropCanvasToBbox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx new file mode 100644 index 00000000000..049853e3f29 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx @@ -0,0 +1,71 @@ +import { MenuGroup } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { ControlLayerMenuItems } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItems'; +import { InpaintMaskMenuItems } from 'features/controlLayers/components/InpaintMask/InpaintMaskMenuItems'; +import { RasterLayerMenuItems } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItems'; +import { IPAdapterMenuItems } from 'features/controlLayers/components/RefImage/IPAdapterMenuItems'; +import { RegionalGuidanceMenuItems } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems'; +import { CanvasEntityStateGate } from 'features/controlLayers/contexts/CanvasEntityStateGate'; +import { + EntityIdentifierContext, + useEntityIdentifierContext, +} from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useEntityTypeString } from 'features/controlLayers/hooks/useEntityTypeString'; +import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import type { PropsWithChildren } from 'react'; +import { memo } from 'react'; +import type { Equals } from 'tsafe'; +import { assert } from 'tsafe'; + +const CanvasContextMenuSelectedEntityMenuItemsContent = memo(() => { + const entityIdentifier = useEntityIdentifierContext(); + + if (entityIdentifier.type === 'raster_layer') { + return ; + } + if (entityIdentifier.type === 'control_layer') { + return ; + } + if (entityIdentifier.type === 'inpaint_mask') { + return ; + } + if (entityIdentifier.type === 'regional_guidance') { + return ; + } + if (entityIdentifier.type === 'reference_image') { + return ; + } + + assert>(false); +}); + +CanvasContextMenuSelectedEntityMenuItemsContent.displayName = 'CanvasContextMenuSelectedEntityMenuItemsContent'; + +const CanvasContextMenuSelectedEntityMenuGroup = memo((props: PropsWithChildren) => { + const entityIdentifier = useEntityIdentifierContext(); + const title = useEntityTypeString(entityIdentifier.type); + + return {props.children}; +}); + +CanvasContextMenuSelectedEntityMenuGroup.displayName = 'CanvasContextMenuSelectedEntityMenuGroup'; + +export const CanvasContextMenuSelectedEntityMenuItems = memo(() => { + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + + if (!selectedEntityIdentifier) { + return null; + } + + return ( + + + + + + + + ); +}); + +CanvasContextMenuSelectedEntityMenuItems.displayName = 'CanvasContextMenuSelectedEntityMenuItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx new file mode 100644 index 00000000000..b8fbb08c020 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx @@ -0,0 +1,87 @@ +import { Grid, GridItem } from '@invoke-ai/ui-library'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; +import { useIsEntityTypeEnabled } from 'features/controlLayers/hooks/useIsEntityTypeEnabled'; +import { newCanvasEntityFromImageDndTarget } from 'features/dnd/dnd'; +import { DndDropTarget } from 'features/dnd/DndDropTarget'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +const addRasterLayerFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({ type: 'raster_layer' }); +const addControlLayerFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({ + type: 'control_layer', +}); +const addRegionalGuidanceReferenceImageFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({ + type: 'regional_guidance_with_reference_image', +}); +const addInpaintMaskFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({ type: 'inpaint_mask' }); +const addResizedControlLayerFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({ + type: 'control_layer', + withResize: true, +}); + +export const CanvasDropArea = memo(() => { + const { t } = useTranslation(); + const isBusy = useCanvasIsBusy(); + const isRasterLayerEnabled = useIsEntityTypeEnabled('raster_layer'); + const isControlLayerEnabled = useIsEntityTypeEnabled('control_layer'); + const isRegionalGuidanceEnabled = useIsEntityTypeEnabled('regional_guidance'); + const isInpaintMaskEnabled = useIsEntityTypeEnabled('inpaint_mask'); + + return ( + <> + + + + + + + + + + + + + + + + + + + ); +}); + +CanvasDropArea.displayName = 'CanvasDropArea'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityContainer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityContainer.tsx new file mode 100644 index 00000000000..f38e78b4448 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityContainer.tsx @@ -0,0 +1,59 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { Box, Flex } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useCanvasEntityListDnd } from 'features/controlLayers/components/CanvasEntityList/useCanvasEntityListDnd'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useEntityIsSelected } from 'features/controlLayers/hooks/useEntityIsSelected'; +import { entitySelected } from 'features/controlLayers/store/canvasSlice'; +import { DndListDropIndicator } from 'features/dnd/DndListDropIndicator'; +import type { PropsWithChildren } from 'react'; +import { memo, useCallback, useRef } from 'react'; + +const sx = { + position: 'relative', + flexDir: 'column', + w: 'full', + bg: 'base.850', + borderRadius: 'base', + '&[data-selected=true]': { + bg: 'base.800', + }, + '&[data-is-dragging=true]': { + opacity: 0.3, + }, + transitionProperty: 'common', +} satisfies SystemStyleObject; + +export const CanvasEntityContainer = memo((props: PropsWithChildren) => { + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext(); + const isSelected = useEntityIsSelected(entityIdentifier); + const onClick = useCallback(() => { + if (isSelected) { + return; + } + dispatch(entitySelected({ entityIdentifier })); + }, [dispatch, entityIdentifier, isSelected]); + const ref = useRef(null); + + const [dndListState, isDragging] = useCanvasEntityListDnd(ref, entityIdentifier); + + return ( + + + {props.children} + + + + ); +}); + +CanvasEntityContainer.displayName = 'CanvasEntityContainer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList.tsx new file mode 100644 index 00000000000..f4cfb7c22ce --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList.tsx @@ -0,0 +1,182 @@ +import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; +import { extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge'; +import { reorderWithEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/util/reorder-with-edge'; +import { Button, Collapse, Flex, Icon, Spacer, Text } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { useBoolean } from 'common/hooks/useBoolean'; +import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; +import { fixTooltipCloseOnScrollStyles } from 'common/util/fixTooltipCloseOnScrollStyles'; +import { CanvasEntityAddOfTypeButton } from 'features/controlLayers/components/common/CanvasEntityAddOfTypeButton'; +import { CanvasEntityMergeVisibleButton } from 'features/controlLayers/components/common/CanvasEntityMergeVisibleButton'; +import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle'; +import { RasterLayerExportPSDButton } from 'features/controlLayers/components/RasterLayer/RasterLayerExportPSDButton'; +import { useEntityTypeInformationalPopover } from 'features/controlLayers/hooks/useEntityTypeInformationalPopover'; +import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTitle'; +import { entitiesReordered } from 'features/controlLayers/store/canvasSlice'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { singleCanvasEntityDndSource } from 'features/dnd/dnd'; +import { triggerPostMoveFlash } from 'features/dnd/util'; +import type { PropsWithChildren } from 'react'; +import { memo, useEffect } from 'react'; +import { flushSync } from 'react-dom'; +import { PiCaretDownBold } from 'react-icons/pi'; + +type Props = PropsWithChildren<{ + isSelected: boolean; + type: CanvasEntityIdentifier['type']; + entityIdentifiers: CanvasEntityIdentifier[]; +}>; + +export const CanvasEntityGroupList = memo(({ isSelected, type, children, entityIdentifiers }: Props) => { + const title = useEntityTypeTitle(type); + const informationalPopoverFeature = useEntityTypeInformationalPopover(type); + const collapse = useBoolean(true); + const dispatch = useAppDispatch(); + + useEffect(() => { + return monitorForElements({ + canMonitor({ source }) { + if (!singleCanvasEntityDndSource.typeGuard(source.data)) { + return false; + } + if (source.data.payload.entityIdentifier.type !== type) { + return false; + } + return true; + }, + onDrop({ location, source }) { + const target = location.current.dropTargets[0]; + if (!target) { + return; + } + + const sourceData = source.data; + const targetData = target.data; + + if (!singleCanvasEntityDndSource.typeGuard(sourceData) || !singleCanvasEntityDndSource.typeGuard(targetData)) { + return; + } + + const indexOfSource = entityIdentifiers.findIndex( + (entityIdentifier) => entityIdentifier.id === sourceData.payload.entityIdentifier.id + ); + const indexOfTarget = entityIdentifiers.findIndex( + (entityIdentifier) => entityIdentifier.id === targetData.payload.entityIdentifier.id + ); + + if (indexOfTarget < 0 || indexOfSource < 0) { + return; + } + + // Don't move if the source and target are the same index, meaning same position in the list + if (indexOfSource === indexOfTarget) { + return; + } + + const closestEdgeOfTarget = extractClosestEdge(targetData); + + // It's possible that the indices are different, but refer to the same position. For example, if the source is + // at 2 and the target is at 3, but the target edge is 'top', then the entity is already in the correct position. + // We should bail if this is the case. + let edgeIndexDelta = 0; + + if (closestEdgeOfTarget === 'bottom') { + edgeIndexDelta = 1; + } else if (closestEdgeOfTarget === 'top') { + edgeIndexDelta = -1; + } + + // If the source is already in the correct position, we don't need to move it. + if (indexOfSource === indexOfTarget + edgeIndexDelta) { + return; + } + + // Using `flushSync` so we can query the DOM straight after this line + flushSync(() => { + dispatch( + entitiesReordered({ + type, + entityIdentifiers: reorderWithEdge({ + list: entityIdentifiers, + startIndex: indexOfSource, + indexOfTarget, + closestEdgeOfTarget, + axis: 'vertical', + }), + }) + ); + }); + + // Flash the element that was moved + const element = document.querySelector(`[data-entity-id="${sourceData.payload.entityIdentifier.id}"]`); + if (element instanceof HTMLElement) { + triggerPostMoveFlash(element, colorTokenToCssVar('base.700')); + } + }, + }); + }, [dispatch, entityIdentifiers, type]); + + return ( + + + + + {informationalPopoverFeature ? ( + + + {title} + + + ) : ( + + {title} + + )} + + + + {type === 'raster_layer' && } + + + + + + + {children} + + + + ); +}); + +CanvasEntityGroupList.displayName = 'CanvasEntityGroupList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx new file mode 100644 index 00000000000..f8fdb7c66cd --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityList.tsx @@ -0,0 +1,22 @@ +import { Flex } from '@invoke-ai/ui-library'; +import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; +import { ControlLayerEntityList } from 'features/controlLayers/components/ControlLayer/ControlLayerEntityList'; +import { InpaintMaskList } from 'features/controlLayers/components/InpaintMask/InpaintMaskList'; +import { RasterLayerEntityList } from 'features/controlLayers/components/RasterLayer/RasterLayerEntityList'; +import { RegionalGuidanceEntityList } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList'; +import { memo } from 'react'; + +export const CanvasEntityList = memo(() => { + return ( + + + + + + + + + ); +}); + +CanvasEntityList.displayName = 'CanvasEntityList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx new file mode 100644 index 00000000000..4a7e6fa3750 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx @@ -0,0 +1,65 @@ +import { IconButton, Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library'; +import { + useAddControlLayer, + useAddInpaintMask, + useAddNewRegionalGuidanceWithARefImage, + useAddRasterLayer, + useAddRegionalGuidance, +} from 'features/controlLayers/hooks/addLayerHooks'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; +import { useIsEntityTypeEnabled } from 'features/controlLayers/hooks/useIsEntityTypeEnabled'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiPlusBold } from 'react-icons/pi'; + +export const EntityListGlobalActionBarAddLayerMenu = memo(() => { + const { t } = useTranslation(); + const isBusy = useCanvasIsBusy(); + const addInpaintMask = useAddInpaintMask(); + const addRegionalGuidance = useAddRegionalGuidance(); + const addRegionalReferenceImage = useAddNewRegionalGuidanceWithARefImage(); + const addRasterLayer = useAddRasterLayer(); + const addControlLayer = useAddControlLayer(); + const isRegionalGuidanceEnabled = useIsEntityTypeEnabled('regional_guidance'); + const isControlLayerEnabled = useIsEntityTypeEnabled('control_layer'); + const isInpaintLayerEnabled = useIsEntityTypeEnabled('inpaint_mask'); + + return ( + + } + data-testid="control-layers-add-layer-menu-button" + isDisabled={isBusy} + /> + + + } onClick={addInpaintMask} isDisabled={!isInpaintLayerEnabled}> + {t('controlLayers.inpaintMask')} + + } onClick={addRegionalGuidance} isDisabled={!isRegionalGuidanceEnabled}> + {t('controlLayers.regionalGuidance')} + + } onClick={addRegionalReferenceImage} isDisabled={!isRegionalGuidanceEnabled}> + {t('controlLayers.regionalReferenceImage')} + + + + } onClick={addControlLayer} isDisabled={!isControlLayerEnabled}> + {t('controlLayers.controlLayer')} + + } onClick={addRasterLayer}> + {t('controlLayers.rasterLayer')} + + + + + ); +}); + +EntityListGlobalActionBarAddLayerMenu.displayName = 'EntityListGlobalActionBarAddLayerMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar.tsx new file mode 100644 index 00000000000..afc663122fd --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar.tsx @@ -0,0 +1,20 @@ +import { Flex } from '@invoke-ai/ui-library'; +import { EntityListSelectedEntityActionBarFill } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill'; +import { EntityListSelectedEntityActionBarOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity'; +import { memo } from 'react'; + +import { EntityListSelectedEntityActionBarCompositeOperation } from './EntityListSelectedEntityActionBarCompositeOperation'; + +export const EntityListSelectedEntityActionBar = memo(() => { + return ( + + + + + + + + ); +}); + +EntityListSelectedEntityActionBar.displayName = 'EntityListSelectedEntityActionBar'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarCompositeOperation.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarCompositeOperation.tsx new file mode 100644 index 00000000000..f82dd530c62 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarCompositeOperation.tsx @@ -0,0 +1,74 @@ +import { FormControl, FormLabel, Select } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { rasterLayerGlobalCompositeOperationChanged } from 'features/controlLayers/store/canvasSlice'; +import { + selectCanvasSlice, + selectEntity, + selectSelectedEntityIdentifier, +} from 'features/controlLayers/store/selectors'; +import type { + CanvasEntityIdentifier, + CanvasRasterLayerState, + CompositeOperation, +} from 'features/controlLayers/store/types'; +import { COLOR_BLEND_MODES } from 'features/controlLayers/store/types'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +const selectCompositeOperation = createSelector(selectCanvasSlice, (canvas) => { + const { selectedEntityIdentifier } = canvas; + + if (selectedEntityIdentifier?.type !== 'raster_layer') { + return 'source-over'; + } + + const entity = selectEntity(canvas, selectedEntityIdentifier); + + return (entity as CanvasRasterLayerState)?.globalCompositeOperation ?? 'source-over'; +}); + +export const EntityListSelectedEntityActionBarCompositeOperation = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const currentOperation = useAppSelector(selectCompositeOperation); + + const onChange = useCallback( + (e: ChangeEvent) => { + if (selectedEntityIdentifier?.type === 'raster_layer') { + const value = e.target.value as CompositeOperation; + + dispatch( + rasterLayerGlobalCompositeOperationChanged({ + entityIdentifier: selectedEntityIdentifier as CanvasEntityIdentifier<'raster_layer'>, + globalCompositeOperation: value, + }) + ); + } + }, + [dispatch, selectedEntityIdentifier] + ); + + if (selectedEntityIdentifier?.type !== 'raster_layer') { + return null; + } + + return ( + + + {t('controlLayers.compositeOperation.label')} + + + + ); +}); + +EntityListSelectedEntityActionBarCompositeOperation.displayName = 'EntityListSelectedEntityActionBarCompositeOperation'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton.tsx new file mode 100644 index 00000000000..2e2f5fa20a4 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton.tsx @@ -0,0 +1,36 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; +import { entityDuplicated } from 'features/controlLayers/store/canvasSlice'; +import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCopyFill } from 'react-icons/pi'; + +export const EntityListSelectedEntityActionBarDuplicateButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isBusy = useCanvasIsBusy(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const onClick = useCallback(() => { + if (!selectedEntityIdentifier) { + return; + } + dispatch(entityDuplicated({ entityIdentifier: selectedEntityIdentifier })); + }, [dispatch, selectedEntityIdentifier]); + + return ( + } + /> + ); +}); + +EntityListSelectedEntityActionBarDuplicateButton.displayName = 'EntityListSelectedEntityActionBarDuplicateButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill.tsx new file mode 100644 index 00000000000..62928f2094f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill.tsx @@ -0,0 +1,88 @@ +import { + Box, + Flex, + Popover, + PopoverBody, + PopoverContent, + PopoverTrigger, + Portal, + Tooltip, +} from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import RgbColorPicker from 'common/components/ColorPicker/RgbColorPicker'; +import { rgbColorToString } from 'common/util/colorCodeTransformers'; +import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle'; +import { entityFillColorChanged, entityFillStyleChanged } from 'features/controlLayers/store/canvasSlice'; +import { selectSelectedEntityFill, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { type FillStyle, isMaskEntityIdentifier, type RgbColor } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const EntityListSelectedEntityActionBarFill = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const fill = useAppSelector(selectSelectedEntityFill); + + const onChangeFillColor = useCallback( + (color: RgbColor) => { + if (!selectedEntityIdentifier) { + return; + } + if (!isMaskEntityIdentifier(selectedEntityIdentifier)) { + return; + } + dispatch(entityFillColorChanged({ entityIdentifier: selectedEntityIdentifier, color })); + }, + [dispatch, selectedEntityIdentifier] + ); + const onChangeFillStyle = useCallback( + (style: FillStyle) => { + if (!selectedEntityIdentifier) { + return; + } + if (!isMaskEntityIdentifier(selectedEntityIdentifier)) { + return; + } + dispatch(entityFillStyleChanged({ entityIdentifier: selectedEntityIdentifier, style })); + }, + [dispatch, selectedEntityIdentifier] + ); + + if (!selectedEntityIdentifier || !fill) { + return null; + } + + return ( + + + + + + + + + + + + + + + + + + + + + + ); +}); + +EntityListSelectedEntityActionBarFill.displayName = 'EntityListSelectedEntityActionBarFill'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton.tsx new file mode 100644 index 00000000000..bb4e809b4d1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton.tsx @@ -0,0 +1,37 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useEntityFilter } from 'features/controlLayers/hooks/useEntityFilter'; +import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { isFilterableEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiShootingStarFill } from 'react-icons/pi'; + +export const EntityListSelectedEntityActionBarFilterButton = memo(() => { + const { t } = useTranslation(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const filter = useEntityFilter(selectedEntityIdentifier); + + if (!selectedEntityIdentifier) { + return null; + } + + if (!isFilterableEntityIdentifier(selectedEntityIdentifier)) { + return null; + } + + return ( + } + /> + ); +}); + +EntityListSelectedEntityActionBarFilterButton.displayName = 'EntityListSelectedEntityActionBarFilterButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarInvertMaskButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarInvertMaskButton.tsx new file mode 100644 index 00000000000..9298a7f149e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarInvertMaskButton.tsx @@ -0,0 +1,44 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; +import { useInvertMask } from 'features/controlLayers/hooks/useInvertMask'; +import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { isMaskEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiSelectionInverseBold } from 'react-icons/pi'; + +export const EntityListSelectedEntityActionBarInvertMaskButton = memo(() => { + const { t } = useTranslation(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const isBusy = useCanvasIsBusy(); + const invertMask = useInvertMask(); + + if (!selectedEntityIdentifier) { + return null; + } + + if (!isMaskEntityIdentifier(selectedEntityIdentifier)) { + return null; + } + + const label = + selectedEntityIdentifier.type === 'regional_guidance' + ? t('controlLayers.invertRegion') + : t('controlLayers.invertMask'); + + return ( + } + /> + ); +}); + +EntityListSelectedEntityActionBarInvertMaskButton.displayName = 'EntityListSelectedEntityActionBarInvertMaskButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity.tsx new file mode 100644 index 00000000000..2b8d787c932 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity.tsx @@ -0,0 +1,193 @@ +import { + $shift, + CompositeSlider, + FormControl, + FormLabel, + IconButton, + NumberInput, + NumberInputField, + Popover, + PopoverAnchor, + PopoverArrow, + PopoverBody, + PopoverContent, + PopoverTrigger, + Portal, +} from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { clamp, round } from 'es-toolkit/compat'; +import { snapToNearest } from 'features/controlLayers/konva/util'; +import { entityOpacityChanged } from 'features/controlLayers/store/canvasSlice'; +import { + selectCanvasSlice, + selectEntity, + selectSelectedEntityIdentifier, +} from 'features/controlLayers/store/selectors'; +import type { KeyboardEvent } from 'react'; +import { memo, useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCaretDownBold } from 'react-icons/pi'; + +function formatPct(v: number | string) { + if (isNaN(Number(v))) { + return ''; + } + + return `${round(Number(v), 2).toLocaleString()}%`; +} + +function mapSliderValueToRawValue(value: number) { + return value / 100; +} + +function mapRawValueToSliderValue(opacity: number) { + return opacity * 100; +} + +function formatSliderValue(value: number) { + return String(value); +} + +const marks = [ + mapRawValueToSliderValue(0), + mapRawValueToSliderValue(0.25), + mapRawValueToSliderValue(0.5), + mapRawValueToSliderValue(0.75), + mapRawValueToSliderValue(1), +]; + +const sliderDefaultValue = mapRawValueToSliderValue(1); + +const snapCandidates = marks.slice(1, marks.length - 1); + +const selectOpacity = createSelector(selectCanvasSlice, (canvas) => { + const selectedEntityIdentifier = canvas.selectedEntityIdentifier; + if (!selectedEntityIdentifier) { + return 1; // fallback to 100% opacity + } + const selectedEntity = selectEntity(canvas, selectedEntityIdentifier); + if (!selectedEntity) { + return 1; // fallback to 100% opacity + } + // Opacity is a float from 0-1, but we want to display it as a percentage + return selectedEntity.opacity; +}); + +export const EntityListSelectedEntityActionBarOpacity = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const opacity = useAppSelector(selectOpacity); + + const [localOpacity, setLocalOpacity] = useState(opacity * 100); + + const onChangeSlider = useCallback( + (opacity: number) => { + if (!selectedEntityIdentifier) { + return; + } + let snappedOpacity = opacity; + // Do not snap if shift key is held + if (!$shift.get()) { + snappedOpacity = snapToNearest(opacity, snapCandidates, 2); + } + const mappedOpacity = mapSliderValueToRawValue(snappedOpacity); + + dispatch(entityOpacityChanged({ entityIdentifier: selectedEntityIdentifier, opacity: mappedOpacity })); + }, + [dispatch, selectedEntityIdentifier] + ); + + const onBlur = useCallback(() => { + if (!selectedEntityIdentifier) { + return; + } + if (isNaN(Number(localOpacity))) { + setLocalOpacity(100); + return; + } + dispatch( + entityOpacityChanged({ entityIdentifier: selectedEntityIdentifier, opacity: clamp(localOpacity / 100, 0, 1) }) + ); + }, [dispatch, localOpacity, selectedEntityIdentifier]); + + const onChangeNumberInput = useCallback((valueAsString: string, valueAsNumber: number) => { + setLocalOpacity(valueAsNumber); + }, []); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + onBlur(); + } + }, + [onBlur] + ); + + useEffect(() => { + setLocalOpacity((opacity ?? 1) * 100); + }, [opacity]); + + return ( + + + + {t('controlLayers.opacity')} + + + + + + } + size="sm" + variant="link" + position="absolute" + insetInlineEnd={0} + h="full" + isDisabled={selectedEntityIdentifier === null} + /> + + + + + + + + + + + + + + ); +}); + +EntityListSelectedEntityActionBarOpacity.displayName = 'EntityListSelectedEntityActionBarOpacity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarSaveToAssetsButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarSaveToAssetsButton.tsx new file mode 100644 index 00000000000..bf222a5f5d0 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarSaveToAssetsButton.tsx @@ -0,0 +1,44 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; +import { useSaveLayerToAssets } from 'features/controlLayers/hooks/useSaveLayerToAssets'; +import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { isSaveableEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiFloppyDiskBold } from 'react-icons/pi'; + +export const EntityListSelectedEntityActionBarSaveToAssetsButton = memo(() => { + const { t } = useTranslation(); + const isBusy = useCanvasIsBusy(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const adapter = useEntityAdapterSafe(selectedEntityIdentifier); + const saveLayerToAssets = useSaveLayerToAssets(); + const onClick = useCallback(() => { + saveLayerToAssets(adapter); + }, [saveLayerToAssets, adapter]); + + if (!selectedEntityIdentifier) { + return null; + } + + if (!isSaveableEntityIdentifier(selectedEntityIdentifier)) { + return null; + } + + return ( + } + /> + ); +}); + +EntityListSelectedEntityActionBarSaveToAssetsButton.displayName = 'EntityListSelectedEntityActionBarSaveToAssetsButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarSelectObjectButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarSelectObjectButton.tsx new file mode 100644 index 00000000000..ca053c704d3 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarSelectObjectButton.tsx @@ -0,0 +1,37 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useEntitySegmentAnything } from 'features/controlLayers/hooks/useEntitySegmentAnything'; +import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { isSegmentableEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiShapesFill } from 'react-icons/pi'; + +export const EntityListSelectedEntityActionBarSelectObjectButton = memo(() => { + const { t } = useTranslation(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const segment = useEntitySegmentAnything(selectedEntityIdentifier); + + if (!selectedEntityIdentifier) { + return null; + } + + if (!isSegmentableEntityIdentifier(selectedEntityIdentifier)) { + return null; + } + + return ( + } + /> + ); +}); + +EntityListSelectedEntityActionBarSelectObjectButton.displayName = 'EntityListSelectedEntityActionBarSelectObjectButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton.tsx new file mode 100644 index 00000000000..0b1009ea0e9 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton.tsx @@ -0,0 +1,37 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useEntityTransform } from 'features/controlLayers/hooks/useEntityTransform'; +import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { isTransformableEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiFrameCornersBold } from 'react-icons/pi'; + +export const EntityListSelectedEntityActionBarTransformButton = memo(() => { + const { t } = useTranslation(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const transform = useEntityTransform(selectedEntityIdentifier); + + if (!selectedEntityIdentifier) { + return null; + } + + if (!isTransformableEntityIdentifier(selectedEntityIdentifier)) { + return null; + } + + return ( + } + /> + ); +}); + +EntityListSelectedEntityActionBarTransformButton.displayName = 'EntityListSelectedEntityActionBarTransformButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityOperationsBar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityOperationsBar.tsx new file mode 100644 index 00000000000..da94b0a6971 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityOperationsBar.tsx @@ -0,0 +1,28 @@ +import { Flex } from '@invoke-ai/ui-library'; +import { EntityListGlobalActionBarAddLayerMenu } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu'; +import { EntityListSelectedEntityActionBarDuplicateButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton'; +import { EntityListSelectedEntityActionBarFilterButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton'; +import { EntityListSelectedEntityActionBarInvertMaskButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarInvertMaskButton'; +import { EntityListSelectedEntityActionBarSelectObjectButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarSelectObjectButton'; +import { EntityListSelectedEntityActionBarTransformButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton'; +import { EntityListNonRasterLayerToggle } from 'features/controlLayers/components/common/CanvasNonRasterLayersIsHiddenToggle'; +import { memo } from 'react'; + +import { EntityListSelectedEntityActionBarSaveToAssetsButton } from './EntityListSelectedEntityActionBarSaveToAssetsButton'; + +export const EntityListSelectedEntityOperationsBar = memo(() => { + return ( + + + + + + + + + + + ); +}); + +EntityListSelectedEntityOperationsBar.displayName = 'EntityListSelectedEntityOperationsBar'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/useCanvasEntityListDnd.ts b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/useCanvasEntityListDnd.ts new file mode 100644 index 00000000000..a036448cd19 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/useCanvasEntityListDnd.ts @@ -0,0 +1,85 @@ +import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; +import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; +import { attachClosestEdge, extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { singleCanvasEntityDndSource } from 'features/dnd/dnd'; +import { type DndListTargetState, idle } from 'features/dnd/types'; +import { firefoxDndFix } from 'features/dnd/util'; +import type { RefObject } from 'react'; +import { useEffect, useState } from 'react'; + +export const useCanvasEntityListDnd = (ref: RefObject, entityIdentifier: CanvasEntityIdentifier) => { + const [dndListState, setDndListState] = useState(idle); + const [isDragging, setIsDragging] = useState(false); + + useEffect(() => { + const element = ref.current; + if (!element) { + return; + } + return combine( + firefoxDndFix(element), + draggable({ + element, + getInitialData() { + return singleCanvasEntityDndSource.getData({ entityIdentifier }); + }, + onDragStart() { + setDndListState({ type: 'is-dragging' }); + setIsDragging(true); + }, + onDrop() { + setDndListState(idle); + setIsDragging(false); + }, + }), + dropTargetForElements({ + element, + canDrop({ source }) { + if (!singleCanvasEntityDndSource.typeGuard(source.data)) { + return false; + } + if (source.data.payload.entityIdentifier.type !== entityIdentifier.type) { + return false; + } + return true; + }, + getData({ input }) { + const data = singleCanvasEntityDndSource.getData({ entityIdentifier }); + return attachClosestEdge(data, { + element, + input, + allowedEdges: ['top', 'bottom'], + }); + }, + getIsSticky() { + return true; + }, + onDragEnter({ self }) { + const closestEdge = extractClosestEdge(self.data); + setDndListState({ type: 'is-dragging-over', closestEdge }); + }, + onDrag({ self }) { + const closestEdge = extractClosestEdge(self.data); + + // Only need to update react state if nothing has changed. + // Prevents re-rendering. + setDndListState((current) => { + if (current.type === 'is-dragging-over' && current.closestEdge === closestEdge) { + return current; + } + return { type: 'is-dragging-over', closestEdge }; + }); + }, + onDragLeave() { + setDndListState(idle); + }, + onDrop() { + setDndListState(idle); + }, + }) + ); + }, [entityIdentifier, ref]); + + return [dndListState, isDragging] as const; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx new file mode 100644 index 00000000000..4dd08edcd58 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx @@ -0,0 +1,32 @@ +import { Divider, Flex } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons'; +import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList'; +import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar'; +import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { selectHasEntities } from 'features/controlLayers/store/selectors'; +import { memo } from 'react'; + +import { EntityListSelectedEntityOperationsBar } from './CanvasEntityList/EntityListSelectedEntityOperationsBar'; +import { ParamDenoisingStrength } from './ParamDenoisingStrength'; + +export const CanvasLayersPanel = memo(() => { + const hasEntities = useAppSelector(selectHasEntities); + + return ( + + + + + + + {!hasEntities && } + {hasEntities && } + + + + + ); +}); + +CanvasLayersPanel.displayName = 'CanvasLayersPanel'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch.tsx new file mode 100644 index 00000000000..13a15363486 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasOperationIsolatedLayerPreviewSwitch.tsx @@ -0,0 +1,28 @@ +import { FormControl, FormLabel, Switch, Tooltip } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + selectIsolatedLayerPreview, + settingsIsolatedLayerPreviewToggled, +} from 'features/controlLayers/store/canvasSettingsSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const CanvasOperationIsolatedLayerPreviewSwitch = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const isolatedLayerPreview = useAppSelector(selectIsolatedLayerPreview); + const onChangeIsolatedPreview = useCallback(() => { + dispatch(settingsIsolatedLayerPreviewToggled()); + }, [dispatch]); + + return ( + + + {t('controlLayers.settings.isolatedPreview')} + + + + ); +}); + +CanvasOperationIsolatedLayerPreviewSwitch.displayName = 'CanvasOperationIsolatedLayerPreviewSwitch'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPasteModal.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasPasteModal.tsx new file mode 100644 index 00000000000..e0f93c6efd9 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasPasteModal.tsx @@ -0,0 +1,149 @@ +import { + Button, + Flex, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, +} from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { useAppSelector, useAppStore } from 'app/store/storeHooks'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; +import { createNewCanvasEntityFromImage } from 'features/imageActions/actions'; +import { toast } from 'features/toast/toast'; +import { atom } from 'nanostores'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiBoundingBoxBold, PiImageBold } from 'react-icons/pi'; +import { useUploadImageMutation } from 'services/api/endpoints/images'; + +const $imageFile = atom(null); +export const setFileToPaste = (file: File) => $imageFile.set(file); +const clearFileToPaste = () => $imageFile.set(null); + +export const CanvasPasteModal = memo(() => { + useAssertSingleton('CanvasPasteModal'); + const { dispatch, getState } = useAppStore(); + const { t } = useTranslation(); + const imageToPaste = useStore($imageFile); + const canvasManager = useCanvasManager(); + const autoAddBoardId = useAppSelector(selectAutoAddBoardId); + const [uploadImage, { isLoading }] = useUploadImageMutation({ fixedCacheKey: 'canvasPasteModal' }); + + const getPosition = useCallback( + (destination: 'canvas' | 'bbox') => { + const { x, y } = canvasManager.stateApi.getBbox().rect; + if (destination === 'bbox') { + return { x, y }; + } + const rasterLayerAdapters = canvasManager.compositor.getVisibleAdaptersOfType('raster_layer'); + if (rasterLayerAdapters.length === 0) { + return { x, y }; + } + { + const { x, y } = canvasManager.compositor.getRectOfAdapters(rasterLayerAdapters); + return { x, y }; + } + }, + [canvasManager.compositor, canvasManager.stateApi] + ); + + const handlePaste = useCallback( + async (file: File, destination: 'assets' | 'canvas' | 'bbox') => { + try { + const is_intermediate = destination !== 'assets'; + const imageDTO = await uploadImage({ + file, + is_intermediate, + image_category: 'user', + board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId, + }).unwrap(); + + if (destination !== 'assets') { + createNewCanvasEntityFromImage({ + type: 'raster_layer', + imageDTO, + dispatch, + getState, + overrides: { position: getPosition(destination) }, + }); + } + } catch { + toast({ + title: t('toast.pasteFailed'), + status: 'error', + }); + } finally { + clearFileToPaste(); + toast({ + title: t('toast.pasteSuccess', { + destination: + destination === 'assets' + ? t('controlLayers.pasteToAssets') + : destination === 'bbox' + ? t('controlLayers.pasteToBbox') + : t('controlLayers.pasteToCanvas'), + }), + status: 'success', + }); + } + }, + [autoAddBoardId, dispatch, getPosition, getState, t, uploadImage] + ); + + const pasteToAssets = useCallback(() => { + if (!imageToPaste) { + return; + } + handlePaste(imageToPaste, 'assets'); + }, [handlePaste, imageToPaste]); + + const pasteToCanvas = useCallback(() => { + if (!imageToPaste) { + return; + } + handlePaste(imageToPaste, 'canvas'); + }, [handlePaste, imageToPaste]); + + const pasteToBbox = useCallback(() => { + if (!imageToPaste) { + return; + } + handlePaste(imageToPaste, 'bbox'); + }, [handlePaste, imageToPaste]); + + return ( + + + + {t('controlLayers.pasteTo')} + + + + + + + + + + + + + + ); +}); + +CanvasPasteModal.displayName = 'CanvasPasteModal'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationModal.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationModal.tsx new file mode 100644 index 00000000000..94a123fa91a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationModal.tsx @@ -0,0 +1,93 @@ +import { + Button, + ButtonGroup, + Flex, + Heading, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Spacer, + Spinner, + Text, +} from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + canvasWorkflowIntegrationClosed, + selectCanvasWorkflowIntegrationIsOpen, + selectCanvasWorkflowIntegrationIsProcessing, + selectCanvasWorkflowIntegrationSelectedWorkflowId, +} from 'features/controlLayers/store/canvasWorkflowIntegrationSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { CanvasWorkflowIntegrationParameterPanel } from './CanvasWorkflowIntegrationParameterPanel'; +import { CanvasWorkflowIntegrationWorkflowSelector } from './CanvasWorkflowIntegrationWorkflowSelector'; +import { useCanvasWorkflowIntegrationExecute } from './useCanvasWorkflowIntegrationExecute'; + +export const CanvasWorkflowIntegrationModal = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const isOpen = useAppSelector(selectCanvasWorkflowIntegrationIsOpen); + const isProcessing = useAppSelector(selectCanvasWorkflowIntegrationIsProcessing); + const selectedWorkflowId = useAppSelector(selectCanvasWorkflowIntegrationSelectedWorkflowId); + + const { execute, canExecute } = useCanvasWorkflowIntegrationExecute(); + + const onClose = useCallback(() => { + if (!isProcessing) { + dispatch(canvasWorkflowIntegrationClosed()); + } + }, [dispatch, isProcessing]); + + const onExecute = useCallback(() => { + execute(); + }, [execute]); + + return ( + + + + + {t('controlLayers.workflowIntegration.title')} + + + + + + + {t('controlLayers.workflowIntegration.description')} + + + + + {selectedWorkflowId && } + + + + + + + + + + + + + ); +}); + +CanvasWorkflowIntegrationModal.displayName = 'CanvasWorkflowIntegrationModal'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationParameterPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationParameterPanel.tsx new file mode 100644 index 00000000000..f59a6c45edb --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationParameterPanel.tsx @@ -0,0 +1,13 @@ +import { Box } from '@invoke-ai/ui-library'; +import { WorkflowFormPreview } from 'features/controlLayers/components/CanvasWorkflowIntegration/WorkflowFormPreview'; +import { memo } from 'react'; + +export const CanvasWorkflowIntegrationParameterPanel = memo(() => { + return ( + + + + ); +}); + +CanvasWorkflowIntegrationParameterPanel.displayName = 'CanvasWorkflowIntegrationParameterPanel'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationWorkflowSelector.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationWorkflowSelector.tsx new file mode 100644 index 00000000000..30bc60605c6 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/CanvasWorkflowIntegrationWorkflowSelector.tsx @@ -0,0 +1,92 @@ +import { Flex, FormControl, FormLabel, Select, Spinner, Text } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + canvasWorkflowIntegrationWorkflowSelected, + selectCanvasWorkflowIntegrationSelectedWorkflowId, +} from 'features/controlLayers/store/canvasWorkflowIntegrationSlice'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useListWorkflowsInfiniteInfiniteQuery } from 'services/api/endpoints/workflows'; + +import { useFilteredWorkflows } from './useFilteredWorkflows'; + +export const CanvasWorkflowIntegrationWorkflowSelector = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const selectedWorkflowId = useAppSelector(selectCanvasWorkflowIntegrationSelectedWorkflowId); + const { data: workflowsData, isLoading } = useListWorkflowsInfiniteInfiniteQuery( + { + per_page: 100, // Get a reasonable number of workflows + page: 0, + }, + { + selectFromResult: ({ data, isLoading }) => ({ + data, + isLoading, + }), + } + ); + + const workflows = useMemo(() => { + if (!workflowsData) { + return []; + } + // Flatten all pages into a single list + return workflowsData.pages.flatMap((page) => page.items); + }, [workflowsData]); + + // Filter workflows to only show those with ImageFields + const { filteredWorkflows, isFiltering } = useFilteredWorkflows(workflows); + + const onChange = useCallback( + (e: ChangeEvent) => { + const workflowId = e.target.value || null; + dispatch(canvasWorkflowIntegrationWorkflowSelected({ workflowId })); + }, + [dispatch] + ); + + if (isLoading || isFiltering) { + return ( + + + + {isFiltering + ? t('controlLayers.workflowIntegration.filteringWorkflows') + : t('controlLayers.workflowIntegration.loadingWorkflows')} + + + ); + } + + if (filteredWorkflows.length === 0) { + return ( + + {workflows.length === 0 + ? t('controlLayers.workflowIntegration.noWorkflowsFound') + : t('controlLayers.workflowIntegration.noWorkflowsWithImageField')} + + ); + } + + return ( + + {t('controlLayers.workflowIntegration.selectWorkflow')} + + + ); +}); + +CanvasWorkflowIntegrationWorkflowSelector.displayName = 'CanvasWorkflowIntegrationWorkflowSelector'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/WorkflowFieldRenderer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/WorkflowFieldRenderer.tsx new file mode 100644 index 00000000000..2d91be13bfa --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowIntegration/WorkflowFieldRenderer.tsx @@ -0,0 +1,548 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { + Combobox, + Flex, + FormControl, + FormLabel, + IconButton, + Input, + Radio, + Select, + Switch, + Text, + Textarea, +} from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { logger } from 'app/logging/logger'; +import { EMPTY_ARRAY } from 'app/store/constants'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { UploadImageIconButton } from 'common/hooks/useImageUploadButton'; +import { + canvasWorkflowIntegrationFieldValueChanged, + canvasWorkflowIntegrationImageFieldSelected, + selectCanvasWorkflowIntegrationFieldValues, + selectCanvasWorkflowIntegrationSelectedImageFieldKey, + selectCanvasWorkflowIntegrationSelectedWorkflowId, +} from 'features/controlLayers/store/canvasWorkflowIntegrationSlice'; +import { DndImage } from 'features/dnd/DndImage'; +import { ModelFieldCombobox } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelFieldCombobox'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import type { NodeFieldElement } from 'features/nodes/types/workflow'; +import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants'; +import { isParameterScheduler } from 'features/parameters/types/parameterSchemas'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiTrashSimpleBold } from 'react-icons/pi'; +import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import { modelConfigsAdapterSelectors, useGetModelConfigsQuery } from 'services/api/endpoints/models'; +import { useGetWorkflowQuery } from 'services/api/endpoints/workflows'; +import type { AnyModelConfig, ImageDTO } from 'services/api/types'; + +const log = logger('canvas-workflow-integration'); + +interface WorkflowFieldRendererProps { + el: NodeFieldElement; +} + +export const WorkflowFieldRenderer = memo(({ el }: WorkflowFieldRendererProps) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const selectedWorkflowId = useAppSelector(selectCanvasWorkflowIntegrationSelectedWorkflowId); + const fieldValues = useAppSelector(selectCanvasWorkflowIntegrationFieldValues); + const selectedImageFieldKey = useAppSelector(selectCanvasWorkflowIntegrationSelectedImageFieldKey); + const templates = useStore($templates); + + const { data: workflow } = useGetWorkflowQuery(selectedWorkflowId!, { + skip: !selectedWorkflowId, + }); + + // Load boards and models for BoardField and ModelIdentifierField + const { data: boardsData } = useListAllBoardsQuery({ include_archived: true }); + const { data: modelsData, isLoading: isLoadingModels } = useGetModelConfigsQuery(); + + const { fieldIdentifier } = el.data; + const fieldKey = `${fieldIdentifier.nodeId}.${fieldIdentifier.fieldName}`; + + log.debug({ fieldIdentifier, fieldKey }, 'Rendering workflow field'); + + // Get the node, field instance, and field template + const { field, fieldTemplate } = useMemo(() => { + if (!workflow?.workflow.nodes) { + log.warn('No workflow nodes found'); + return { field: null, fieldTemplate: null }; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const foundNode = workflow.workflow.nodes.find((n: any) => n.data.id === fieldIdentifier.nodeId); + if (!foundNode) { + log.warn({ nodeId: fieldIdentifier.nodeId }, 'Node not found'); + return { field: null, fieldTemplate: null }; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const foundField = (foundNode.data as any).inputs[fieldIdentifier.fieldName]; + if (!foundField) { + log.warn({ nodeId: fieldIdentifier.nodeId, fieldName: fieldIdentifier.fieldName }, 'Field not found in node'); + return { field: null, fieldTemplate: null }; + } + + // Get the field template from the invocation templates + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nodeType = (foundNode.data as any).type; + const template = templates[nodeType]; + if (!template) { + log.warn({ nodeType }, 'No template found for node type'); + return { field: foundField, fieldTemplate: null }; + } + + const foundFieldTemplate = template.inputs[fieldIdentifier.fieldName]; + if (!foundFieldTemplate) { + log.warn({ nodeType, fieldName: fieldIdentifier.fieldName }, 'Field template not found'); + return { field: foundField, fieldTemplate: null }; + } + + return { field: foundField, fieldTemplate: foundFieldTemplate }; + }, [workflow, fieldIdentifier, templates]); + + // Get the current value from Redux or fallback to field default + const currentValue = useMemo(() => { + if (fieldValues && fieldKey in fieldValues) { + return fieldValues[fieldKey]; + } + + return field?.value ?? fieldTemplate?.default ?? ''; + }, [fieldValues, fieldKey, field, fieldTemplate]); + + // Get field type from the template + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fieldType = fieldTemplate ? (fieldTemplate as any).type?.name : null; + + const handleChange = useCallback( + (value: unknown) => { + dispatch(canvasWorkflowIntegrationFieldValueChanged({ fieldName: fieldKey, value })); + }, + [dispatch, fieldKey] + ); + + const handleStringChange = useCallback( + (e: ChangeEvent) => { + handleChange(e.target.value); + }, + [handleChange] + ); + + const handleNumberChange = useCallback( + (e: ChangeEvent) => { + const val = fieldType === 'IntegerField' ? parseInt(e.target.value, 10) : parseFloat(e.target.value); + handleChange(isNaN(val) ? 0 : val); + }, + [handleChange, fieldType] + ); + + const handleBooleanChange = useCallback( + (e: ChangeEvent) => { + handleChange(e.target.checked); + }, + [handleChange] + ); + + const handleSelectChange = useCallback( + (e: ChangeEvent) => { + handleChange(e.target.value); + }, + [handleChange] + ); + + // SchedulerField handlers + const handleSchedulerChange = useCallback( + (v) => { + if (!isParameterScheduler(v?.value)) { + return; + } + handleChange(v.value); + }, + [handleChange] + ); + + const schedulerValue = useMemo(() => SCHEDULER_OPTIONS.find((o) => o.value === currentValue), [currentValue]); + + // BoardField handlers + const handleBoardChange = useCallback( + (v) => { + if (!v) { + return; + } + const value = v.value === 'auto' || v.value === 'none' ? v.value : { board_id: v.value }; + handleChange(value); + }, + [handleChange] + ); + + const boardOptions = useMemo(() => { + const _options: ComboboxOption[] = [ + { label: t('common.auto'), value: 'auto' }, + { label: `${t('common.none')} (${t('boards.uncategorized')})`, value: 'none' }, + ]; + if (boardsData) { + for (const board of boardsData) { + _options.push({ + label: board.board_name, + value: board.board_id, + }); + } + } + return _options; + }, [boardsData, t]); + + const boardValue = useMemo(() => { + const _value = currentValue; + const autoOption = boardOptions[0]; + const noneOption = boardOptions[1]; + if (!_value || _value === 'auto') { + return autoOption; + } + if (_value === 'none') { + return noneOption; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const boardId = typeof _value === 'object' ? (_value as any).board_id : _value; + const boardOption = boardOptions.find((o) => o.value === boardId); + return boardOption ?? autoOption; + }, [currentValue, boardOptions]); + + const noOptionsMessage = useCallback(() => t('boards.noMatching'), [t]); + + // ModelIdentifierField handlers + const handleModelChange = useCallback( + (value: AnyModelConfig | null) => { + if (!value) { + return; + } + handleChange(value); + }, + [handleChange] + ); + + const modelConfigs = useMemo(() => { + if (!modelsData) { + return EMPTY_ARRAY; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ui_model_base = fieldTemplate ? (fieldTemplate as any)?.ui_model_base : null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ui_model_type = fieldTemplate ? (fieldTemplate as any)?.ui_model_type : null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ui_model_variant = fieldTemplate ? (fieldTemplate as any)?.ui_model_variant : null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ui_model_format = fieldTemplate ? (fieldTemplate as any)?.ui_model_format : null; + + if (!ui_model_base && !ui_model_type) { + return modelConfigsAdapterSelectors.selectAll(modelsData); + } + + return modelConfigsAdapterSelectors.selectAll(modelsData).filter((config) => { + if (ui_model_base && !ui_model_base.includes(config.base)) { + return false; + } + if (ui_model_type && !ui_model_type.includes(config.type)) { + return false; + } + if (ui_model_variant && 'variant' in config && config.variant && !ui_model_variant.includes(config.variant)) { + return false; + } + if (ui_model_format && !ui_model_format.includes(config.format)) { + return false; + } + return true; + }); + }, [modelsData, fieldTemplate]); + + // ImageField handler + const handleImageFieldSelect = useCallback(() => { + dispatch(canvasWorkflowIntegrationImageFieldSelected({ fieldKey })); + }, [dispatch, fieldKey]); + + if (!field || !fieldTemplate) { + log.warn({ fieldIdentifier }, 'Field or template is null - not rendering'); + return null; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const label = (field as any)?.label || (fieldTemplate as any)?.title || fieldIdentifier.fieldName; + + // Log the entire field structure to understand its shape + log.debug( + { fieldType, label, currentValue, fieldStructure: field, fieldTemplateStructure: fieldTemplate }, + 'Field info' + ); + + // ImageField - allow user to select which one receives the canvas image + if (fieldType === 'ImageField') { + return ( + + ); + } + + // Render different input types based on field type + if (fieldType === 'StringField') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const isTextarea = (fieldTemplate as any)?.ui_component === 'textarea'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const isRequired = (fieldTemplate as any)?.required ?? false; + + if (isTextarea) { + return ( + + {label} +